Quick Start
Four steps to a live order: sign up, activate your player domain, mint a launch token, embed it.
Correction
The hosted fast path this page used to describe — the one that needed no DNS work — no longer exists.
Until recently this chapter told you to mint a token and embed a URL on a PredMask-hosted player origin, with your own domain presented as an optional upgrade you could do later. That shape is retired. Every merchant now serves its players from its own domain, activated before the first launch, and there is no shared origin left to fall back to. A merchant without an enabled domain does not get a working URL on someone else's host — the launch call refuses with merchant_domain_not_ready and hands back nothing to embed.
If you built against the older description, the change that matters to you is sequencing, not code: the DNS record, the certificate and the domain review in step 2 are now on the critical path in sandbox as well as production, and they gate everything after them.
Four steps take you from a signed contract to a player placing a real order in the sandbox. They are the same four in either wallet model — hosted or self-hosted only changes who holds the money and who implements the hooks, not how a player reaches the game. See Wallet modes for that choice, which you make in step 1.
Get signed up, pick a wallet model, collect your keys
Nothing here is self-service: the operator provisions a sandbox merchant for you. Two of the three items below are decisions rather than deliverables, and neither is a flag you can flip later — make them deliberately instead of discovering them in step 3.
- • Your wallet model. Hosted means player balances sit on the operator's ledger — money commands settle inside the platform and you write no callback hooks. Self-hosted means funds stay in your ledger and the operator calls your wallet hooks for every debit and credit. This is a property of your merchant record, not a flag you flip in a request.
- • The host your players will use. Decide the exact hostname now —
play.sandbox.your-domain.comor whatever you prefer — because step 2 registers that string and the whole player surface is bound to it. Read the cookie warning in Custom domain (CNAME) before you pick a subdomain rather than after. - • One API key/secret pair per environment. Sandbox keys answer
401against production and the reverse, with no hint that the key simply belongs elsewhere.
API_BASE_URL = https://api.sandbox.predmask.com # the host you sign requests against
API_KEY = ak_9dc9a497... # B2B access key
API_SECRET = <32+ bytes> # signing secret — server-side only, never in a browser
# That is the whole list. Two things you might expect here are deliberately absent:
#
# merchant id — never sent. Your merchant is derived from API_KEY on every request,
# so there is nothing to configure and nothing to get wrong. You will
# still see the id in your own data; it is not a credential.
#
# game client URL — not fixed, and not yours to build. Step 3 returns a game_url on
# the domain you activated in step 2, so a domain change needs no
# redeploy on your side. There is no default to fall back to: with no
# enabled domain the call refuses instead of returning a URL.
#
# Two runtime strings deserve the same care as the secret, though they are short-lived:
# launch_token and game_url. The URL embeds the token, so logging the URL logs a live
# session credential. We never write either to our own logs; do the same on your side.Configure and activate your sandbox domain
This is the step that decides whether your merchant is player-ready, and it is mostly not code. You submit the host from the integration page of your merchant console and point one CNAME at our edge; the operator then verifies the parts a platform cannot verify about itself from the inside and runs activation. Sandbox and production are registered and reviewed separately — an enabled sandbox domain grants nothing in production.
You can hold one pending registration at a time, and you can withdraw your own as long as it has never been enabled — so a typo costs a resubmission, not a support ticket. If the host you are taking over was previously another merchant's, activation additionally needs a DNS TXT ownership proof; raise that during onboarding, because the value does not exist until the registration does. Both are covered in Custom domain (CNAME).
play.sandbox.your-domain.com. CNAME <edge hostname — issued by the operator>.
; The edge hostname is issued to you, not derived: it differs per environment and per
; deployment, so there is no value to guess or copy from another integration.
; Do not use an A record. A request that reaches an origin without passing the edge has
; skipped tenant routing, the certificate check and the embedding policy in one step.Four things have to hold before the domain can go live, and activation refuses on the first one that does not:
- • DNS. Your CNAME resolves to the edge hostname you were given.
- • Certificate. A certificate at the edge covers that exact host. Enable a domain before this is in place and your players meet a certificate warning outside the application entirely, where none of your monitoring is watching.
- • Host and SNI agree. Tenant identity comes from the Host, so a request whose Host and SNI disagree cannot be attributed safely and is not accepted as a tenant request.
- • Embedding policy. The parent origins that may frame the game are registered per host and emitted as
frame-ancestors. A host with none registered gets'none'rather than a permissive default — list your parent pages here, including any preview deployment you intend to test from.
Activation then exercises the domain for real, inside the same transaction as the status flip: one sa1 credential exchange and one launch-token exchange against your host. Both must pass; either leg failing rolls the whole thing back and the domain stays pending. There is no state in which a domain reads as enabled but has never served a successful entry — Custom domain (CNAME) walks the states, the two canary legs and every refusal reason.
Until this passes, there is nothing to launch
A pending domain is on file and not live: anonymous browsing on that host answers 404 and the session exchange answers 401, exactly as for a host nobody ever submitted. Step 3 refuses in the same period. Treat domain activation as a lead-time item scheduled ahead of the sprint that needs it: DNS propagation, certificate issuance and the operator's review run on their own clocks, not on your build's.
Mint a launch token from your backend
When a player opens the game, your server mints a one-time launch token with your B2B key. The player is named in the body as player_id and created on first use, so there is no separate provisioning call, and the merchant comes from the key — no console credentials are involved anywhere in this flow. Sign the exact body bytes you send. The token is single-use and short-lived — expires_in tells you the window, 60 seconds in production — so mint one per launch rather than caching it.
These four steps describe the embedded model, where the player's browser holds the session. If instead your own backend will call the player endpoints — quoting, ordering and closing on the player's behalf — you never mint a launch token at all: take a session at POST /b2b/v1/auth/session and send it as Authorization: Bearer. See Authentication.
BODY='{"player_id":"PLAYER-123"}' # sign these exact bytes, then send them
curl -X POST "$API_BASE_URL/b2b/v1/auth/launch-token" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
# player_id is your own stable id for the player, not a UUID we issue.
# There is no X-Player-Id header any more: sending one is refused with
# 400 player_id_header_retired, before the signature is even checked.{
"launch_token": "nTpntEN2ixa7Zodgmyc4iDk4y75dAE3PEDQ7Q7yx7PE",
"expires_in": 60,
"game_url": "https://play.sandbox.your-domain.com/?lt=nTpntEN2ixa7Zodgmyc4iDk4y75dAE3PEDQ7Q7yx7PE"
}The host inside game_url is resolved per merchant at mint time and is always your one enabled domain. You do not build it, and it is not a constant to bake into a client: pointing a new CNAME at us changes it with no redeploy on your side.
No enabled domain is a refusal, not a fallback
If your merchant has no enabled domain — step 2 not finished, or the domain later withdrawn — this call answers a stable merchant_domain_not_ready and returns no game_url at all. It never substitutes a shared platform address. That is deliberate: a URL on a host that is not yours would attribute your players' traffic to another tenant boundary, carry the wrong embedding policy and break the moment your own domain went live. Handle it as a configuration state to surface to your operations team, not as a transient error to retry in a loop — retrying changes nothing until the domain is enabled.
- Your backend
POST /b2b/v1/auth/launch-tokenSigned with your B2B key, with {"player_id":"…"} as the body. Returns launch_token, expires_in (60 in production), and a game_url whose host is your one enabled domain — or merchant_domain_not_ready if you have none.
- The player's browser
GET game_url ( …/?lt=<token> )This is the one leg where the token rides in a URL. Do not log it, do not cache it, do not put it anywhere a page refresh can replay — mint a fresh one per launch.
- game-client
exchanges lt for a player sessionServed from your domain, so the session, the player API and the realtime feed are all same-origin. The token burns on first use and expires after expires_in seconds either way: a game_url is good for exactly one load.
Load game_url in your page and place a real order
Render game_url as returned. The game-client redeems the launch token for a session — player identity, currency and a live price feed — and because it is served from your own host, everything it calls afterwards is same-origin: you configure no path rules, proxy nothing and maintain no route table that can drift out of step with ours.
<iframe
src="https://play.sandbox.your-domain.com/?lt=nTpntEN2ixa7Zodgmyc4iDk4y75dAE3PEDQ7Q7yx7PE"
allow="clipboard-write"
style="width:100%;height:100%;border:0"
title="PredMask"
></iframe>Two failures show up here for the first time, and neither is a token problem. A blocked frame — the console says an ancestor violates frame-ancestors — means the page doing the framing is not in the ancestor list registered for that host in step 2. A blank frame with a 404 in the network panel means the Host did not resolve to an enabled domain at all. Both are answered on the domain side, before any credential is examined.
Redemption burns the token: it is consumed atomically on first use and expires after expires_in seconds either way. So a game_url is good for exactly one load. Do not cache it, do not put it in a link someone might revisit, and do not let a page refresh re-render the same one — mint a fresh token server-side each time the iframe is created, or the second load lands on a token that no longer exists.
That's the whole loop.
The player picks a market, stakes from the wallet you chose in step 1, and PredMask prices, holds the exposure and settles the outcome — crediting winnings back through that same wallet.
On timing, honestly: once you are signed and your domain is enabled, the code above is a short development cycle's work — one signed server-side call and one iframe, plus the wallet hooks if you self-host. The rest of the elapsed time belongs to steps nobody here controls: DNS propagation, certificate issuance and the operator's domain review. Sequence the domain first and the code is the small part; sequence it last and it is the only part that matters.
Next: Custom domain (CNAME) for step 2 in full, Launch & sessions for the token and session contract, and Wallet modes when you are ready to hold funds yourself.