Skip to main content

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.

Never send this from a browser

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" } }
ScopeRoutes
games:readGET /games
battles:readGET /battles, GET /battles/{id}
battles:writePOST /battles, /seats, /leave, /cancel
bots:writePOST /battles/{id}/bot-seats
viewer:mintPOST /battles/{id}/viewer-tokens
demo:mintPOST /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

ConditionStatusCode
No Authorization header401unauthorized
Unknown, revoked or expired key401unauthorized
Valid key, missing scope403forbidden
Valid key, suspended tenant403forbidden

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