API overview
The server-to-server API is what your casino backend talks to. It is small: one catalog route, seven battle routes, and a token route.
| Base URL | Your instance, or the one your platform host gave you |
| Auth | Authorization: Bearer sbk_... |
| Content type | application/json |
| Body limit | 1 MiB |
| Errors | {"error":{"code":"...","message":"..."}} on every failure |
The routes
| Route | Scope | What it does |
|---|---|---|
GET /games | games:read | The catalog, filtered to your allowlist |
POST /battles | battles:write | Open a lobby |
GET /battles | battles:read | List your battles |
GET /battles/{id} | battles:read | Read one |
POST /battles/{id}/seats | battles:write | Seat a player |
POST /battles/{id}/bot-seats | bots:write | Fill the rest with bots |
POST /battles/{id}/leave | battles:write | Free a seat |
POST /battles/{id}/cancel | battles:write | Cancel an open lobby |
POST /battles/{id}/viewer-tokens | viewer:mint | Mint a live-feed token |
There is no start endpoint. A battle starts itself once every seat is READY. See
Battle lifecycle.
Four behaviours to know before you start
Money is a string. entry_amount is a canonical decimal string, never a JSON number. It
is also normalised: send "25.50" and every response returns "25.5". Compare amounts by
parsing them as decimals, never by string equality.
Bodies are decoded strictly. Unknown fields are a 400, not a silent ignore. Sending
tenant_id in a POST /battles body fails with unknown field "tenant_id", because the
tenant comes from your API key and nowhere else.
An empty allowlist means an empty catalog. GET /games on a casino with nothing allowed
returns {"games": []} and every battle creation fails game_not_allowed. That is
configuration, not breakage. An allowlist change made in the console appears at once; one made
for you by your host can take up to 30 seconds to reach this API.
Another casino's battle answers 404, not 403. A 403 would confirm the id exists,
turning a permission error into an id-enumeration oracle.
A minimal integration
Four calls open a battle, fill it, and get the audience watching:
BASE=https://slotbattle.example.com
KEY=sbk_3f9a1c...
# 1. What may we play?
curl -s "$BASE/games" -H "Authorization: Bearer $KEY"
# 2. Open a lobby. The creator is seated at index 0.
BATTLE=$(curl -s "$BASE/battles" \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"game_id":"sweet-bonanza","seats_total":3,"entry_amount":"25.50",
"currency":"BRL","creator_player_ref":"alice",
"creator_game_url":"https://provider.example/session/abc"}')
ID=$(echo "$BATTLE" | jq -r .id)
# 3. Seat another player.
curl -s "$BASE/battles/$ID/seats" \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"player_ref":"bob","game_url":"https://provider.example/session/def"}'
# 4. Fill what is left with bots — this is usually what starts the battle.
curl -s "$BASE/battles/$ID/bot-seats" \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{}'
The battle is now running. Two things happen next, and they reach different places:
- Viewers connect to the WebSocket feed with the
ws_tokenfrom step 2 or 3. - Your backend receives one signed webhook when the battle ends.
Settle on the webhook, never on a WebSocket frame. See Events.
Idempotency
No route takes an Idempotency-Key header. Make retries safe the ordinary way:
POST /battles/{id}/seats is naturally idempotent per player, since a player who already
holds a READY seat is rejected with 409 rather than being seated twice.
Rate limits
The S2S surface has no rate limiter. The console's authentication routes are throttled, but
battle creation is bounded by capacity rather than by rate: an instance at its in-flight limit
answers 503 unavailable, which is retryable with backoff.
Next
- Authentication: keys, scopes and the failure modes
- Battles: the lobby routes in detail
- WebSocket: the live feed
- Webhooks: the terminal result
- Errors: every code, and what to do about it