Custom domain (CNAME)

Required before any player can launch: point one CNAME at our edge, pass the domain review, and serve the whole player surface same-origin on your own host.

Every PredMask integration serves the game from a domain of yours — play.your-brand.com — which you point at our edge with one CNAME record. The domain is not branding. It is how a request is attributed to your merchant, and it is what makes the player surface same-origin: the iframe, the player API and the realtime feed all live on that one host, so the browser never makes a cross-origin call at all. A host we do not have on file is not an unstyled version of the game; it is a 404.

Read this before you plan the work, not after

This page used to describe an optional upgrade — something to do once the integration worked on a platform-hosted origin. It is now a prerequisite for every merchant, in every environment, which is why it sits first in Integration rather than alongside the optional material. There is exactly one enabled domain per merchant per environment and no shared origin behind it: until yours is enabled, POST /b2b/v1/auth/launch-token refuses with merchant_domain_not_ready and no player can be launched at all.

It is also the step with the longest lead time and the least code. DNS propagation, certificate issuance and the human review below run on their own clocks — schedule them ahead of the sprint that needs them. The Quick Start places this as step 2 of four for the same reason.

One record, per environment

Point the host at our edge with a CNAME. Do not resolve it to an origin address with 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, and nothing downstream can reconstruct them.

DNS — one record per environmentdns
; sandbox
play.sandbox.your-brand.com.   CNAME   <edge hostname — issued by the operator>.

; production
play.your-brand.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 — take it from your onboarding contact and use it verbatim. Registration records the hostname you were told to expect, and the review below compares your live record against it.

Sandbox and production domains are registered separately and reviewed separately. Nothing about an enabled sandbox domain carries over. See Environments & endpoints for what else is separate between the two.

Registration and review

Registration is yours; activation is ours. You submit the host yourself from the integration page of your merchant console — there is no operator endpoint that creates a registration, so nothing about this step waits on us — and the operator then reviews and activates it. A domain moves through three states, and the path between them is deliberately narrow.

  1. 1. You submit the host you want to serve from, in your console. It is normalised to lowercase without a port, and it must be a real multi-label name. The registry is globally unique across live registrations: a name held by any registration that has not been withdrawn — under any merchant, pending, enabled or disabled alike — is refused as a conflict rather than re-pointed. You may hold one pending registration at a time; a second submission is refused until the first is activated or withdrawn, and withdrawing is the way out of a typo rather than waiting for a review that will never pass.
  2. 2. The row is created in pending. Nothing about it is live: the exchange on that host answers 401 and anonymous browsing answers 404, exactly as for a host that was never submitted.
  3. 3. Once your CNAME resolves and the certificate covers the host, the operator runs activation. It checks the conditions below in order and refuses on the first one that does not hold.
  4. 4. Inside the same transaction as the status flip, a canary exercises the domain for real. If it does not pass, the whole activation is rolled back and the domain stays pending.
  5. 5. Only then is the domain enabled, and the credential, embedding and realtime bindings at the bottom of this page start to apply to it.

state 1

pending

On file, not live. The only state activation will accept as a starting point.

state 2

enabled

Serving. Host resolves to your merchant, credentials signed for this host are accepted, embedding policy is emitted for your parent sites.

state 3

disabled

Withdrawn. Cannot be switched back on — it must be resubmitted to pending and pass the whole review again.

What activation checks, in order:

  • The row is pending. A disabled domain is refused here — that is the rule that makes withdrawal one-way.
  • Your merchant is enabled. Fail-closed: if the merchant record cannot be read at all, activation is refused rather than assumed.
  • Ownership is proven, if this name was ever someone else's. A name whose earlier registration belonged to a different merchant needs a fresh DNS TXT proof before it can be activated — see below. First registrations, and re-registrations by the same merchant, do not go through this.
  • Certificate, Host/SNI consistency and frame-ancestors are confirmed. These three are signed off by a human against live output, because the platform cannot verify its own edge from the inside. They are what the operator is testing when they ask you to leave the record in place and wait.
  • Your CNAME is probed once and compared against the expected edge hostname. This one is advisory: a probe that times out or disagrees is recorded in the audit trail rather than blocking, because DNS propagation and split-horizon resolvers make it an unreliable gate. The canary is the gate.
  • The canary passes. Blocking, and the only check that exercises the real path end to end.

The canary — why enabled is never a paper state

Activation performs, against your host, exactly one sa1 credential exchange and exactly one launch-token exchange. Both must succeed. Either leg failing rolls the transaction back — the status flip included — so the domain returns to pending with the failing leg and its reason written to the audit trail. There is no state in which a domain is marked enabled but has never served a successful entry.

The two legs are not redundant. They are the two entry mechanisms — a locally signed credential and a console-minted token — and they fail for different reasons: the sa1 leg covers signature verification, the clock window, the Host binding, merchant status and nonce burn in one shot, while the launch-token leg covers minting and redemption. A domain that passes one and not the other is a real and specific finding, which is why the result records them separately.

Two side effects are worth knowing before you see them. The probe runs under a reserved player id, pp-domain-canary, so that id appears under your merchant after a domain is activated; it never holds funds and never places an order. And the session the canary creates is revoked immediately after the activation commits, so it does not sit in your session list and does not consume the one-session-per-player slot of any real player.

One enabled domain, and what happens with none

A merchant has exactly one enabled domain per environment, and that is the host POST /b2b/v1/auth/launch-token builds game_url on. There is no primary flag to set and no second domain to choose between: enabling a replacement retires the previous one in the same transaction, so there is never a window with two.

With none enabled, the launch call refuses — a stable merchant_domain_not_ready, with no game_url in the response. It does not fall back to a platform-hosted origin; there is no longer one to fall back to. If you are working from an older description that promised a working-but-unbranded link in this situation, that behaviour is gone, and so is the failure mode it caused: a launch URL quietly pointing somewhere unexpected after a domain change.

The honest consequence of a single entry is that changing domains is a cutover with a visible seam, not a zero-downtime migration. The old host is refused the instant the new one is enabled, and every iframe already open on it needs a fresh launch to come back. Mitigate it the way you would any DNS cutover — lower the TTL in advance, complete the certificate, Host/SNI and ancestor checks on the new host before the switch, pick a window, and have the parent page rebuild its iframes afterwards. It also means availability has to be solved on that one host: certificate renewal you watch, edge failover, and continuous DNS and TLS probing. A second tenant entry is not the answer, and is not offered.

Disabling is one-way

A domain can be disabled at any time and stops serving immediately. It cannot be switched back on. Restoring it means resubmitting it to pending and passing the entire review again, canary included. That is deliberate — a domain is disabled because something about it stopped being true (the certificate, the DNS record, the edge association, the relationship), and none of those come back just because the row is flipped. What a disabled registration keeps is the history: it stays on file under your merchant, it still holds the name, and it is the object you resubmit. Only the operator can remove one — see the next section for why that boundary sits where it does.

Plan the DNS side with it. A domain that is disabled while its CNAME still points at our edge is a dangling record; retire the record at the same time, and do not create the record before the domain has been registered, so that the name is never resolving to an edge that has nothing on file for it.

Withdrawing a registration, and reusing a name

A name is held by live registrations only. Withdrawing one releases it: the row becomes a tombstone that keeps the audit history but leaves the uniqueness key, so the same host can be registered again afterwards — by you, or by somebody else. A re-registration is always a new registration with a new id; a withdrawn one is never revived, and no setting on it survives.

Who can withdraw what follows the same principle as the rest of the lifecycle — you can undo what was never reviewed, and the operator handles anything that has been live.

  • You, from your console: your own pending registration, and only if it has never been enabled. That last part is a separate check rather than a reading of the current status, because a disabled registration can legitimately be resubmitted back to pending — without it, two individually legal steps would add up to erasing a domain you had actually run on.
  • The operator: any pending or disabled registration. An enabled one is refused outright — disable it first, which keeps “still serving” and “removed” from ever being the same moment.

Console writes on domains — submit, resubmit and withdraw — share one abuse budget: ten per ten minutes and thirty per UTC day, per merchant, counted on attempts rather than on successes, and answering 429 past either. A normal integration spends three or four of these in its life; you will only meet the limit by scripting a submit/withdraw loop, which is what the budget is there to stop.

If the name was previously another merchant's, you prove you control it

Because a withdrawn registration releases the name, a host can move between merchants — and the previous tenant's CNAME often outlives their integration. That dangling record still points at our edge, so “the CNAME resolves to us” proves nothing about who controls the name today. Activation therefore requires a fresh DNS TXT proof whenever the name carries a withdrawn registration belonging to a different merchant.

The operator issues a challenge — a record name and a random value, generated for your registration specifically — and passes it to you; you publish it as a TXT record on that host; the operator verifies it. A proof is good for 24 hours, and re-issuing a challenge invalidates any earlier proof, so treat it as something to do immediately before activation rather than weeks ahead. Activation without a valid proof is refused with a 400 naming the missing or expired challenge, and no amount of retrying changes that — the record has to go up.

This never applies to a first registration, nor to re-registering a name that was only ever yours. If you are taking over a host from another party, raise it during onboarding: it is the one prerequisite here that needs a DNS change you cannot make in advance, because the value does not exist until the registration does.

What the domain is bound to, once enabled

The credential is bound to the host

The h field of an sa1 credential must equal the Host the exchange arrives on — lowercase, no port — and that domain must be enabled under the merchant that signed. A perfectly signed credential delivered to a different domain is refused, which is what stops one merchant's signature from opening a session on another merchant's domain.

Embedding policy follows the registration

frame-ancestors is emitted at the edge per host, allowing your parent sites and no one else's. A host with no registered ancestors gets 'none' rather than a permissive default, and X-Frame-Options is deliberately not set alongside it — it cannot express a list, and the two together behave inconsistently in older browsers.

The realtime feed is bound three ways

A WebSocket handshake is accepted only when the Host is an enabled domain, that domain belongs to the same merchant as the ticket, and it matches the host the session was opened on. A ticket proves who holds the session, not which tenant domain it is being used from — so the Host is checked separately.

An unknown host is a 404, not a 401

Anonymous market reads resolve your merchant from the Host alone. An unregistered host, a pending or disabled one, or a disabled merchant all answer 404 — there is no header that names the tenant instead, and there is no fallback. The status is the fastest way to tell a domain problem apart from a credential problem, which is always a 401.

Two things same-origin changes for you

Serving the player surface from your own host is what makes the browser's calls same-origin. That buys one thing and costs one thing, and both are worth reading before you pick the host name.

You configure no path rules

/c-api/* and the WebSocket endpoint are routed on that host by our edge, not by anything of yours. You do not proxy them, you do not enumerate paths, and you do not maintain a route table that can drift out of step with ours when the client changes. Your entire side of the arrangement is the CNAME record.

Your parent-domain cookies now reach this host

A cookie you set with Domain=.your-brand.com is sent by the browser to play.your-brand.com like any other subdomain — and one that is not HttpOnly can additionally be read by script running inside the iframe. This is inherent to hosting a third-party surface on your own name; it is not specific to us and no configuration on our side removes it.

The mitigation is on the cookie, not on the domain: do not put anything sensitive in a parent-domain cookie. Scope session and identity cookies to the exact host that needs them, mark them HttpOnly and Secure, and if you already run wildcard-domain cookies, decide about them before you choose a subdomain rather than after. Our edge does not forward a Cookie header to the platform backend, but that governs what we receive — it does not stop the browser attaching yours on the way.

When it does not work

Domain faults and credential faults look identical from inside the player's browser — both are usually a bare failure with no reason. The status code separates them, so read that first.

FieldTypeDescription
player sees an empty game, network shows 404404optionalThe Host did not resolve to an enabled domain. Either it was never registered, it is still pending, it was disabled, or your merchant itself is disabled. A typo in the host, a staging subdomain that was never submitted and a certificate served for a different name all land here. Nothing about the credential is involved yet.
exchange answers 401 on a domain that browses fine401optionalThe domain resolves, so the fault is in the credential's binding to it. The h field must equal the Host exactly — lowercase, no port, no trailing dot — and the domain must belong to the merchant whose key signed. A host that works anonymously but refuses every exchange is almost always h built from a config constant that no longer matches the domain actually being served.
the iframe is blocked in the browser consoleCSPoptionalRefused to frame … because an ancestor violates … frame-ancestors. Your parent site is not in the ancestor list registered for that host. It is emitted per host from the registry, so a new marketing domain, a preview deployment on a generated hostname, or a page moved to a different origin all need the ancestor list updating first. A host with no registered ancestors is refused outright rather than defaulted open.
activation refused: domain already registered409optionalThe name is held by a live registration — pending, enabled or disabled, and not necessarily yours. Re-submitting never overwrites it: the existing registration has to be withdrawn first, by you if it is your own never-enabled pending row, otherwise by the operator. Once it is withdrawn the name is free again and you register it afresh, as a new registration; if the previous holder was a different merchant, activation will then ask for a DNS TXT ownership proof.
activation refused: ownership not proven400optionalThis host carries a withdrawn registration from a different merchant, so a fresh DNS TXT proof is required and there either is none or the one on file is more than 24 hours old. Ask the operator to issue (or re-issue) the challenge, publish the TXT record, have it verified, then activate — in that order, because re-issuing invalidates any previous proof. Nothing about your CNAME or certificate is wrong when you see this.
activation refused: not pending400optionalThe row is disabled, and disabled cannot go straight back to enabled. It must be resubmitted to pending first and then pass the full review again, canary included. This is also what you will hit if two people activate the same domain concurrently — the second one finds the row already moved.
activation refused: canary failed409optionalThe status flip was rolled back and the domain is still pending. Read which leg failed. The sa1 leg fails on signature, clock window, Host binding, merchant status or nonce; the launch-token leg fails on minting or redemption. A canary that fails while everything looks configured usually means the edge is not yet routing that host to us, or is routing it somewhere that answers on its own.
players see a certificate warning or a blank pageTLSoptionalThe certificate at the edge does not cover the host yet, or has expired. This is the failure mode your players find before your monitoring does, because it happens outside the application entirely. Do not enable a domain before the certificate is in place, and watch the expiry the same way you watch your own.

Where this sits in the integration

The domain gates everything the player touches and nothing your servers touch. The B2B surface you sign against does not use it and the money path does not either — those keep working on a merchant whose domain is still pending, which is exactly why an integration can look healthy from your backend while no player can be launched. Once the domain is enabled, return to the Quick Start at step 3 and mint a launch token; if you sign sa1 credentials yourself, set h to this host.

POST/c-api/v1/session/exchangeRuns on your domain. Refuses a credential whose h is not this host, or whose signing merchant does not own it.
GET/c-api/v1/marketsAnonymous market data, attributed to your merchant by the Host alone. 404 on any host that is not enabled.
WS/c-api/v1/ws/ticks?ticket=…Handshake bound three ways: enabled host, ticket's merchant, and the host the session was opened on.
POST/b2b/v1/auth/launch-tokenBuilds game_url on your one enabled domain. With none enabled it refuses — merchant_domain_not_ready, no URL returned and no shared origin substituted.