Environments & endpoints
Sandbox and production base URLs, which surface talks to which host, and the limits that apply to every call.
There are two environments — sandbox and production — and they are separate systems, not two modes of one. The part that catches people is not the base URL, though: it is that not every side of the integration talks to the API domain at all. Your backend does. The player's browser does not.
The hosts
| Sandbox | Production | |
|---|---|---|
| B2B API — you call us | https://api.sandbox.predmask.com | https://api.predmask.com |
| Player origin — your enabled CNAME | play.sandbox.your-domain.com | play.your-domain.com |
| Wallet hooks — we call you | your wallet_base_url | your wallet_base_url |
Every example in this book is written against the sandbox hosts. Only the first row is a fixed address of ours, and it is fixed per environment: moving to production is a change of base URL and credentials, not a change of contract. Your credentials are issued per environment and are not interchangeable.
The other two rows are yours, and neither is an address you can copy from this page. The player origin is the domain you registered and activated — one enabled CNAME per merchant per environment — and it reaches you inside game_url from the launch call, never constructed by hand. And wallet hooks run on your host, not ours — the operator calls the wallet_base_url you registered, so nothing about that leg involves the API domain in either direction.
There is no platform-hosted player entry, in either environment. Earlier versions of this table published one, and integrations were built against it as a fallback — that row is gone because the thing it named is gone. A merchant with no enabled domain does not get a URL on a shared host: the launch call refuses with merchant_domain_not_ready and returns nothing to embed. Plan the domain before the code that needs it — see Custom domain (CNAME).
Which side uses the API domain
This is the distinction worth getting right before you write any client code, because guessing it wrong produces failures that look like authentication problems and are not.
server to server
Your backend → the API domain
Everything under /b2b/v1 — minting launch tokens, orders, quotes, positions, settlements — is signed with your B2B key and sent to the API host in the table above. This is the only surface a base URL of ours belongs in your configuration.
in the browser
The player → your own domain
The embedded game, the player API under /c-api/v1 and the realtime feed are all served on your registered CNAME domain, routed there by our edge. The player's browser never calls the API domain, and the Host it does call is what attributes the request to your merchant.
This is not a preference, and there is no cross-origin fallback. The player API answers no CORS headers at all and has no preflight routes, so a page or an iframe served from any other origin cannot call it from a browser regardless of what credentials it holds. Tenant identity comes from the Host, which means a request arriving on a host we do not have on file cannot be attributed to anyone: it answers 404, not 401. If you are building against an older description of this — a browser talking straight to the API domain with a merchant header — that shape is retired, header and all.
https, everywhere, with no fallback
Every base URL in this book is https, and no http variant of any host is published. Treat a plaintext base URL in your configuration as a bug rather than a degraded mode: it is not a supported form of these hosts, so a client that quietly downgrades does not get a weaker connection — it gets a broken one.
The rule is also enforced on the URLs you register with us, which is where it bites in practice. A wallet_base_url on http is rejected outright when you save it — 400, with a message naming TLS — and the only exemption is a loopback address for local development. It is not a production-only check and there is no flag that relaxes it: the wallet hook is the channel money instructions travel on, so it is held to TLS in every environment. The same save-time validation refuses private, loopback, link-local and cloud-metadata addresses, and redirects are never followed at call time. See Wallet hooks for what that failure looks like from your side, which is nothing at all.
The two environments share nothing
- Separate data. Merchants, players, markets, orders and settlements exist independently in each. Nothing is copied across, in either direction, at any point — a sandbox player id and the same string in production are two unrelated accounts.
- Separate credentials, and they are not interchangeable. The
access_key/secretpair issued for one environment answers401against the other — a flat unauthorized with no hint that the key simply belongs elsewhere, which is why a mixed-up base URL is one of the classic causes of an afternoon spent debugging a signature that was correct all along. Keep the base URL and the key in the same block of configuration so they cannot drift apart. - Separate domain registrations. Your domain is registered and reviewed per environment, so a working sandbox domain grants nothing in production — and until one is enabled in an environment, nothing can be launched to a player there at all. It is the one prerequisite you cannot satisfy from code. See Custom domain (CNAME).
- Sandbox first, deliberately. The sandbox exists so that a version of your integration can be exercised against real platform behaviour with disposable data before it touches real money. Certification runs there, and production access follows it rather than preceding it.
Telling “not deployed” from “not authorised”
The two environments do not always carry the same build, so a call that works in one can 404 in the other for a reason that has nothing to do with your request. Probe with an unauthenticated, empty request and read the status: it separates the two cases without needing a valid signature.
curl -s -o /dev/null -w '%{http_code}\n' -m 30 \
--retry 4 --retry-all-errors --retry-delay 2 \
-X POST "https://api.sandbox.predmask.com/b2b/v1/auth/launch-token"
# 401 → the route exists and wants authentication → deployed
# 400 → the route exists and wants valid arguments → deployed
# 404 → not deployed in this environmentAn occasional 000 from curl is a TLS handshake that timed out, not an endpoint that is down — hence the retry flags above. Treat a single one as noise and a run of them as a network path worth looking at.
Limits that apply in both
These are identical in sandbox and production, which makes the sandbox a fair place to discover them.
| Field | Type | Description | |
|---|---|---|---|
| request rate | 429 | optional | 300 requests per 10 seconds, fixed window, counted per source IP address — one bucket shared by every surface, not a per-merchant or per-key quota. Only /healthz and /metrics are exempt. Over the limit answers too_many_requests with a Retry-After header whose value is the whole window; no X-RateLimit headers are sent. Budget your polling against this before you design it — see the B2B API page for what it means for a player base behind one egress address. |
| timestamp window | 401 | optional | X-Timestamp must be within ±300 seconds of platform time, and the same window governs the embed credential's ts. Your clock is your responsibility: there is no handshake that corrects drift, and a drifted signer fails identically to a bad key. |
| nonce lifetime | 401 | optional | A nonce is single-use and remembered for 601 seconds — twice the clock window plus one — scoped per access key. Re-sending an identical signed envelope after a timeout therefore fails; re-sign instead, keeping the same idem_key in the body. |
| request body | 413 | optional | 1 MiB. The cap is applied before the signature is checked and the body is never truncated and then verified, so an oversized request is refused outright rather than silently altered. |
| source IP | 403 | optional | If your merchant runs an IP allowlist, calls from an unlisted address are refused with 403 rather than 401 — the split is deliberate, so status alone tells a source problem from a signing one. Under the strict policy an empty list rejects everything. |
Because the rate limit counts addresses rather than accounts, it is your egress topology that decides your real headroom, not your contract. The B2B API page works that through, along with the fixed-window edge cases; the error codes page separates this 429 from the unrelated one that means a lease is held elsewhere.
Where the base URL shows up
Every signed example elsewhere in this book uses one of the hosts above — this page is the single place they are defined, so a curl that does not work is worth checking against this table before anything else. The B2B canonical is signed over the path, never the host, so switching environments means changing the base URL and the key and nothing else in your signer. See Authentication for the canonical itself and Signature self-check for telling a wrong-environment key apart from a wrong canonical.
# sandbox
API_BASE_URL = https://api.sandbox.predmask.com
API_KEY = ak_... # issued for sandbox — 401 anywhere else
API_SECRET = <32+ bytes> # server-side only, never in a browser
# production — use the values delivered with your merchant, not these
API_BASE_URL = https://api.predmask.com
API_KEY = ak_...
API_SECRET = <32+ bytes>