# Embedding a game The frame and your page exchange a small, fixed set of messages. Both sides check the origin of every message before trusting it. ## Place the frame ``` ``` > The frame’s own `` is your registered account name, not ours; the `<iframe>`’s own `title` attribute is yours to set, and is what assistive technology announces. The path segment is your partner id; the endpoint takes no credentials. The test environment has its own path, `/embed/test/acme`, with its own origin list. The frame is served from a different origin than your page: your page cannot read the game’s token or DOM, and the game cannot read yours. ## If you sandbox the frame If you sandbox the frame, two tokens are required: | Token | Why the frame needs it | | --- | --- | | `allow-scripts` | The game is a script; without this nothing runs. | | `allow-same-origin` | Without it the frame gets an *opaque* origin — nobody’s. Its own scripts and stylesheet fail to load, and every storage call throws. | Anything beyond those two is yours to decide — we do not need popups, forms, modals, downloads, or top-level navigation. The separate `allow` attribute is a different mechanism (Permissions Policy). Include `clipboard-write` for the frame’s copy-link buttons to work; without it they fail quietly. ## Register your origins first Every origin hosting a page that embeds the frame must be registered in [the portal](/docs/portal.md) — exact scheme, host, and port if not default, including staging and preview — per environment: a test origin cannot embed the live frame. > **An unregistered origin is not something your code can catch.** The embed request still returns `200` with a normal HTML document; the browser — not our server — refuses to render it, via the `frame-ancestors` policy. There is no error for your JavaScript to see — check the devtools console, where the violation is logged. An account with *no* origins registered for an environment is embeddable *nowhere* — the state a brand-new account starts in. The request answers `409 Conflict` naming the environment, visible to a `curl` or deploy check, unlike the case above. An unknown or deactivated partner id answers a plain `404` with no document. **A change is not instant.** It is saved the moment you press the button, but takes up to **60 seconds** to reach the frame, and up to **10 minutes** if our registry cannot be read. ## Messages from the game | Message | What it means | | --- | --- | | `{ type: "ready" }` | Loaded, waiting for an identity — send `init`. | | `{ type: "needToken" }` | Token is getting old, or was rejected. Mint a fresh one for the same player and game and reply with a `token` message — expect this around the halfway point of the hour, or after backgrounding the tab. | | `{ type: "exit", to? }` | The player asked to leave — navigate on your site. | | `{ type: "chrome", sidebar }` | Entering or leaving an immersive match, asking your navigation out of the way. Honour it or ignore it. | `sidebar`: **`false` hides** your navigation (entering a match); `true` restores it (match over). A request about your layout, never a requirement — ignoring it still works. `to`, when present, is a **path on your own site** beginning with `/` — never an absolute URL, never a route inside the game. **Validate it anyway**: refuse anything that does not start with a single `/` and fall back to your own games page. Navigating to a string from a frame without checking it is an open redirect, and that is true however trustworthy the sender. > **Message types are additive under the version policy.** One more, `seatRequest`, is under [The seat request](#the-seat-request) below. Match only the types you handle: a switch with no default, or a throw on unknown, breaks on a change that breaks nobody else. > There is no per-match score message; the outcome is in [the results feed](/docs/results.md), read server-to-server and computed from the moves as replayed. ## Messages to the game ``` frame.postMessage({ type: "init", token, // session token for this player and this game uid, // the uid the token was issued for dealerUrl, // required: https://api.peritusgames.com gameId, // optional route, // optional: deep link — "/play/" + matchId seating, // optional: "host" — see below backAtRoot,// optional: "exit" — see below }, GAME_ORIGIN) ``` `seating: "host"` tells the game **you** take the money here: it may show tier cards, and a tap asks you to confirm and charge. Leave it out for no match entry — solo practice, plus whatever you seat server-side and deep-link with `route`. Any other value is treated as absent. `backAtRoot: "exit"` draws a back arrow on the game’s first screen, which posts `{ type: "exit" }` with no `to` — you choose where the player lands. Leave it out and that screen has no arrow. Inside the game the arrow is ours and always there: it moves the player between the lobby and a match and never reaches your page. Any other value is treated as absent. `route` on `init` runs the other way: a path inside the GAME, not your site like `exit`’s `to`. The only shape you need is on [Paid matches](/docs/paid-play.md) — `"/play/" + matchId`. Send `{ type: "token", token }` on its own whenever you refresh the session, including in reply to `needToken` — the frame already has the uid, so this is not a second `init`. ## The seat request With `seating: "host"` set, a tap on a tier card sends one more message from the game — the only one that carries money: ``` { type: "seatRequest", nonce: "…", // opaque id for this request; nothing to reply to gameId: "streakpeaks", tierId: "usd_10", attempt: "1724…-uuid", // derive your idempotencyKey from this } ``` Confirm and charge on your own side, then seat the match from your server — the whole sequence is on [Paid matches](/docs/paid-play.md). The frame does not wait for a `postMessage` reply: it detects the seat through our own API, which is why nothing your page posts back can seat a player. ## Check the origin, both ways Always pass an explicit target origin to `postMessage`, and always compare `event.origin` against the game origin before acting — anything on your page can post to your window. The frame does the same: it only accepts `init` from a registered origin, and says so plainly if yours is missing, rather than sitting in its loading state.