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​