Authentication
Every S2S route takes a tenant API key as a bearer token.
Authorization: Bearer sbk_0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9
The key is sbk_ followed by 64 hex characters. Only its SHA-256 hash is stored, so it cannot
be recovered: the plaintext is shown once, at creation.
It is a server-to-server credential scoped to your whole casino. Anything holding it can open battles, seat bots and read every battle you have. Proxy calls through your own backend; the tutorial shows the pattern.
The tenant comes from the key
There is no tenant header on this surface and no tenant field to send. The key identifies the casino, and everything the request can reach is scoped to it.
Sending one anyway does not work around this: bodies are decoded strictly, so an extra
tenant_id field is rejected with 400 unknown field "tenant_id".
Scopes
Each key carries its own set. A route whose scope is missing answers 403 and names it:
{ "error": { "code": "forbidden", "message": "missing scope: battles:write" } }
| Scope | Routes |
|---|---|
games:read | GET /games |
battles:read | GET /battles, GET /battles/{id} |
battles:write | POST /battles, /seats, /leave, /cancel |
bots:write | POST /battles/{id}/bot-seats |
viewer:mint | POST /battles/{id}/viewer-tokens |
demo:mint | POST /games/{id}/demo-sessions |
bots:write is not covered by battles:write. Seating bots spends the casino's own provider
sessions, which is a different act from opening a lobby.
demo:mint mints a demo game session for a game in your catalog. It is not part of the
production flow, where a real player's session comes from your own login, so it exists for
demos, smoke tests and support and is gated separately. A production integration does not need
it.
Issue the narrowest set that works
A backend that only renders battles needs games:read and battles:read. One that also opens
them needs battles:write. Give each service its own key: revoking a shared key takes every
consumer down at once.
Failure modes
| Condition | Status | Code |
|---|---|---|
No Authorization header | 401 | unauthorized |
| Unknown, revoked or expired key | 401 | unauthorized |
| Valid key, missing scope | 403 | forbidden |
| Valid key, suspended tenant | 403 | forbidden |
401 is uniform: absent, malformed, unknown, revoked and expired all answer the same body.
Distinguishing them would tell an attacker which guesses were close.
A suspended tenant is the one case that gets a specific answer, tenant suspended, because
the caller is legitimate and needs to know to stop retrying.
Issuing and revoking
From the console: Casino → API keys. Costs apikeys.write; reading the list costs
apikeys.read. The plaintext is shown once, on screen, and never again.
If your account does not carry apikeys.write, ask your host to issue and revoke for you.
Revocation takes effect immediately, with no cache to wait out.
Every issue and revoke records who did it. A key your host issued outside the console has no
signed-in operator and appears in the list attributed to CLI / unknown rather than a blank
cell.
Rotating a key
There is no in-place rotation. Issue the replacement, deploy it, then revoke the old one, in that order. Revoking first means downtime for as long as the deploy takes.
Next
- Keys and credentials: every credential and how it rotates
- Errors: the full code catalog
- API keys in the console