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
<iframe
src="https://games.peritusgames.com/embed/acme" <!-- live -->
sandbox="allow-scripts allow-same-origin"
allow="fullscreen; clipboard-write"
title="Streak Peaks"
></iframe>The frame’s own <title> 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 — 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 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, 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 — "/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. 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.