Skip to main content

Battles

Seven routes drive the whole lobby, and none of them is a start: a battle starts itself once every seat is READY. Read Battle lifecycle first.

Open a battle

POST /battles scope: battles:write → 201
{
"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"
}

seats_total must be 2 to 8. The creator is seated at index 0 automatically.

creator_game_url decides whether the battle can ever start

Supply it and seat 0 is READY. Omit it and seat 0 is FILLED: claimed, but unable to play. A two-seat battle created without it will not start when the other player joins, because seat 0 is still not READY.

Omit it only if you intend to call POST /battles/{id}/seats for the creator later.

The response is the full battle, including a ws_token for the live feed:

{
"id": "btl_uCHSnowFVUXf73YaSH2j",
"status": "OPEN",
"entry_amount": "25.5",
"ws_url": "wss://slotbattle.example.com/ws?battle_id=btl_uCHSnowFVUXf73YaSH2j",
"ws_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImsxIn0...",
"ws_token_expires_at": "2026-08-03T03:55:42Z",
"ws_subprotocol": "slotbattle.jwt",
"seats": [
{ "seat_index": 0, "seat_ref": "usr_s0", "player_ref": "alice", "is_bot": false, "status": "READY" },
{ "seat_index": 1, "seat_ref": "usr_s1", "player_ref": "", "is_bot": false, "status": "EMPTY" },
{ "seat_index": 2, "seat_ref": "usr_s2", "player_ref": "", "is_bot": false, "status": "EMPTY" }
],
"expires_at": "2026-08-03T03:47:10Z"
}

"25.50" came back as "25.5", because amounts are normalised. expires_at is the lobby window: unfilled by then, the battle is cancelled.

ws_token can be missing

If the instance has no viewer signing key, the battle is created normally and ws_token is absent. It is an omitempty field, so there is no error to catch, and live viewing is unavailable until a key is configured. Check for its presence rather than assuming it.

Failures worth handling

StatusCodeMeaning
400invalid_requestseats_total outside 2..8, missing creator_player_ref or game_id, or an unknown field
403game_not_allowedThe game is not in your allowlist
409conflictThe creator is already in another active battle
503unavailableAt capacity. Retry with backoff

Seat a player

POST /battles/{id}/seats scope: battles:write → 200
{ "player_ref": "bob", "game_url": "https://provider.example/session/def" }

Takes the lowest-index empty seat, or upgrades the caller's existing FILLED seat to READY. Both fields are required, so the seat is always READY afterwards, which is what can trigger auto-start.

A player holding a READY or PLAYING seat cannot join again (409). One holding a FILLED seat is upgrading, which is allowed and is how a creator who omitted creator_game_url gets themselves ready.

The response carries a fresh ws_token, so the joining player's browser can start watching immediately.

Fill the rest with bots

POST /battles/{id}/bot-seats scope: bots:write → 200

Fills every remaining empty seat. Bots get demo sessions minted under your casino's own provider credentials, so they land READY, which is normally what starts the battle.

{}

An empty body auto-names every bot. To name them:

{
"player_ref": "alice",
"bots": [{ "player_ref": "bot_lucky" }, { "player_ref": "bot_swift" }]
}

Supplying bots means supplying at least as many entries as there are empty seats. Fewer is a 409.

Two gates apply here and nowhere else:

  • Creator only. A player_ref that is not the creator is a 403.
  • The allowlist is re-checked. A game revoked while the lobby sat open stops the battle at this point rather than letting it run.

A 502 means minting a bot's demo session failed at the provider, which is an upstream problem rather than a bad request.

Leave

POST /battles/{id}/leave scope: battles:write → 200
{ "player_ref": "bob" }

Frees the seat back to EMPTY, clearing its player, video URL and any result.

If the creator leaves, the battle is cancelled

Leaving does not hand the lobby to someone else. Check creator_player_ref before calling this on a creator's behalf, and make sure your UI says "cancel", not "leave".

Cancel

POST /battles/{id}/cancel scope: battles:write → 200

Creator only, OPEN only. A battle that already started cannot be cancelled here, and answers 409.

The body is optional. Send {"player_ref": "alice"} when the call comes from a player action, so the creator check runs. Omit it for an operator-initiated cancel, which skips that check.

Read and list

GET /battles/{id} scope: battles:read
GET /battles?status=OPEN&limit=20 scope: battles:read

limit defaults to 20 and is clamped to 100: asking for more is capped silently, not rejected. Results are newest first.

Neither route mints a ws_token. Mint one explicitly with viewer tokens when a viewer needs to connect to a battle they did not create.

A battle belonging to another casino answers 404, identically to one that never existed.

Do not poll

Do not poll GET /battles/{id} to follow a battle. Subscribe to the WebSocket feed for live state, and treat the webhook as the authority on the result. Reserve reads for rendering a page someone just opened.

Next