Skip to Content
External GamesContract & SDK

Contract & SDK

This document describes the canonical contract between ResultFly and an external game delivered through the ExternalGame component. It also outlines the lightweight SDK surface we expose to make implementation easier for partners.

Lifecycle overview

  1. Navigation — user reaches a page node that contains the external block. The runtime constructs the iframe URL with context query parameters.
  2. Iframe boot — once the iframe load event fires, ResultFly sends a postMessage with type: "resultfly:init" to the iframe’s contentWindow.
  3. Ready acknowledgement — the game inspects the payload and answers with type: "external-game.ready". Until this happens the wrapper shows a loading state.
  4. Gameplay — the parties exchange messages defined below (state updates, score reports, resizing, pause/resume).
  5. Completion — the game announces completion or an unrecoverable error. The wrapper transitions the UI and records analytics; the hosting flow may continue to the next node based on studio logic.

If the iframe never acknowledges the init message, the wrapper times out after the configured timeoutMs and surfaces an error block to the user.

Initialization payload

resultfly:init has the following structure:

type InitPayload = { session: { id: string; // Unique ResultFly session id nodeId: string; // Current page node id componentId: string; // ExternalGame instance id locale: string; // e.g. "en-US" preview: boolean; // True in studio preview }; user: { participantId: string; // Stable identifier for analytics traits?: Record<string, string | number | boolean | null>; }; experiment?: { campaignId: string; groupId?: string; }; config: unknown; // Editors' initPayload blob theme: { palette: Record<string, string>; typographyScale: string; }; };

Fields mirror data already available to native controls. Most properties are read-only and meant for analytics correlation rather than tampering.

Messages sent by ResultFly

TypeWhenPayload
resultfly:initImmediately after iframe loadInitPayload
resultfly:pauseWhen the host page is hidden or paused by Studio{ reason: "visibilitychange" | "manual" }
resultfly:resumeWhen the page becomes active again{}
resultfly:update-configWhen editors tweak initPayload in preview without reload{ config: unknown }

Games must ignore unknown message types to stay forward-compatible.

Messages sent by the game

All outbound messages must use postMessage with targetOrigin matching the parent origin the iframe booted from. The wrapper rejects messages whose origin is not in the allowedOrigins list configured on the component.

TypeRequiredPurposePayload
external-game.readySignals that assets are loaded and the UI can be shown.{ capabilities?: string[] }
external-game.progressInform ResultFly about intermediate milestones (levels, lives, etc.).{ step: string; value?: number; meta?: Record<string, unknown> }
external-game.scoreProvide a numeric score for leaderboards or rewards.{ value: number; unit?: string; meta?: Record<string, unknown> }
external-game.completeGameplay finished successfully.{ outcome: "win" | "loss" | "neutral"; durationMs?: number; rewards?: Record<string, unknown> }
external-game.error✅ on failureReport unrecoverable issues. Triggers fallback UI.{ code: string; message?: string; detail?: unknown }
external-game.resizeRequest a height change once layout is known.{ height: number }
external-game.logOptional debugging hook visible only in Studio preview.{ level: "info" | "warn" | "error"; message: string; payload?: unknown }

The wrapper automatically enriches each inbound message with session metadata before forwarding it to analytics/event buses.

SDK surface

To avoid hand-writing postMessage plumbing, we expose a minimal SDK that games can import. The SDK provides:

import { createResultFlyGame } from "@resultfly/external-game-sdk"; const game = createResultFlyGame(); game.onInit(({ session, config }) => { // start rendering once data is ready }); game.reportScore({ value: 42 }); game.complete({ outcome: "win" });
  • createResultFlyGame() registers message listeners and verifies the parent origin automatically.
  • onInit, onPause, and onResume helpers wrap the inbound messages listed above.
  • reportProgress, reportScore, complete, fail, resize, and log helpers format outbound messages and guard against rate limits (e.g., no more than 1 resize per 250 ms).
  • During development the SDK can run in “strict” mode to throw descriptive errors when the contract is violated.

The SDK itself is optional; custom frameworks can speak the protocol directly as long as messages follow the schema.

Security and sandboxing

  • ResultFly enforces sandbox attributes to keep the iframe isolated from the host document. allow-same-origin is disabled unless the target origin is explicitly trusted by the organization.
  • Only HTTPS URLs are accepted. Mixed-content loads are blocked at publish time.
  • The wrapper validates the origin of every inbound postMessage and silently ignores unknown or malformed payloads.
  • If the iframe navigates to a different origin at runtime, communication stops until the new origin matches the allowlist. This prevents phishing or malicious redirects.
  • The platform can inject a watchdog that tears down the iframe if it uses more than the allotted execution time or memory (values TBD), ensuring one runaway game does not compromise the overall experience.

Validation and publishing workflow

  1. Authoring — editors add the component, provide URLs, and optionally drop in the developer’s configuration JSON. Studio warns if required fields are missing.
  2. Preview handshake — when the page runs in preview, Studio verifies that external-game.ready arrives within the timeout and that completion/error messages conform to the schema. Failures surface inline so editors can notify the developer.
  3. Publishing checks — the build system persists the allowlist, timeout, and schema version alongside the experience. It also stores a checksum of the SDK contract (for auditability) in platform-spec.
  4. Runtime telemetry — analytics events include a componentType: "external_game" tag and the componentId, so we can monitor MTTR/SLA per partner.

Only after the block passes preview validation is the “Publish” button enabled, ensuring that external code is contract-compliant before it reaches end users.

Last updated on