Skip to main content

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 URLYour instance, or the one your platform host gave you
AuthAuthorization: Bearer sbk_...
Content typeapplication/json
Body limit1 MiB
Errors{"error":{"code":"...","message":"..."}} on every failure

Full reference with Try It

The routes

RouteScopeWhat it does
GET /gamesgames:readThe catalog, filtered to your allowlist
POST /battlesbattles:writeOpen a lobby
GET /battlesbattles:readList your battles
GET /battles/{id}battles:readRead one
POST /battles/{id}/seatsbattles:writeSeat a player
POST /battles/{id}/bot-seatsbots:writeFill the rest with bots
POST /battles/{id}/leavebattles:writeFree a seat
POST /battles/{id}/cancelbattles:writeCancel an open lobby
POST /battles/{id}/viewer-tokensviewer:mintMint 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_token from 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