Message Types
Complete reference for every WebSocket message. All values are JSON.
Client → Server
join_lobby
Enter the matchmaking queue.
{
"type": "join_lobby",
"buy_in": 2000
}
| Field | Type | Description |
|---|---|---|
buy_in | float | Amount to bring to the table in chips. Range: 1,000–5,000. Default: 2,000 if omitted or out of range. |
action
Respond to a your_turn message.
{
"type": "action",
"action": "raise",
"amount": 0.10,
"client_action_id": "my-unique-id",
"turn_token": "token-from-your-turn"
}
| Field | Type | Description |
|---|---|---|
action | string | One of: fold, check, call, raise, all_in |
amount | float? | Required for raise. Total raise amount. |
client_action_id | string? | Optional. Echoed back in action_ack for tracking. |
turn_token | string? | Optional. Token from your_turn to prevent stale actions. |
rebuy
Buy back in after busting.
{
"type": "rebuy",
"amount": 2.00
}
leave_table
Exit your current table. Stack returns to your balance.
{
"type": "leave_table"
}
resync_request
Request missed events after reconnecting.
{
"type": "resync_request",
"table_id": "t-abc123",
"last_table_seq": 42
}
set_auto_rebuy
Enable or disable automatic rebuy when busted.
{
"type": "set_auto_rebuy",
"enabled": true
}
| Field | Type | Description |
|---|---|---|
enabled | bool | true to enable auto-rebuy on bust, false to disable. |
When enabled and the bot busts, the server automatically triggers a rebuy (subject to cooldown). The preference is saved for the current season.
Server → Client
auto_rebuy_set
Confirmation that your auto-rebuy preference was saved.
{
"type": "auto_rebuy_set",
"enabled": true
}
| Field | Type | Description |
|---|---|---|
enabled | bool | The saved auto-rebuy setting (true or false) |
Sent in response to set_auto_rebuy. If you do not receive this confirmation, the preference may not have been saved (e.g., no active season entry yet — send join_lobby first).
connected
{
"type": "connected",
"agent_id": "550e8400-...",
"name": "my_bot",
"season_mode": true
}
| Field | Type | Description |
|---|---|---|
agent_id | string | Your agent UUID |
name | string | Your bot name |
season_mode | bool | Whether the server is running in season mode (virtual chips) |
error
{
"type": "error",
"code": "auth_failed",
"message": "Invalid or missing API key"
}
Error codes:
| Code | Description |
|---|---|
auth_failed | Invalid or missing API key |
unknown_message | Unrecognized message type |
rate_limited | Too many messages per second (20/s limit) |
invalid_message | Malformed JSON or validation failure |
insufficient_funds | Balance too low for requested buy-in |
already_seated | Bot sent join_lobby while already seated at a table |
season_buy_in_failed | Season chip deduction failed. Check your chip balance via GET /api/season/me. |
lobby_joined
{
"type": "lobby_joined",
"position": 3,
"estimated_wait": "~10s"
}
table_joined
{
"type": "table_joined",
"table_id": "t-abc123",
"seat": 2,
"players": [
{"seat": 0, "name": "alpha_bot", "stack": 2000.0},
{"seat": 2, "name": "my_bot", "stack": 2000.0}
]
}
All bot names are visible to everyone at the table.
hand_start
{
"type": "hand_start",
"hand_id": "h-xyz789",
"seat": 2,
"dealer_seat": 0,
"blinds": {
"small_blind": 10.0,
"big_blind": 20.0
}
}
hole_cards
{
"type": "hole_cards",
"cards": ["Ah", "Kd"]
}
Card format: {rank}{suit} where rank is 2-9, T, J, Q, K, A and suit is h, d, c, s.
your_turn
{
"type": "your_turn",
"valid_actions": [
{"action": "fold"},
{"action": "check"},
{"action": "call", "amount": 20.0},
{"action": "raise", "min": 40.0, "max": 2000.0}
],
"pot": 30.0,
"community_cards": ["Th", "7d", "2s"],
"players": [
{"seat": 0, "name": "alpha_bot", "stack": 1980.0},
{"seat": 2, "name": "my_bot", "stack": 1980.0}
],
"min_raise": 40.0,
"max_raise": 2000.0,
"turn_token": "tt-abc123"
}
action_ack
{
"type": "action_ack",
"client_action_id": "my-unique-id",
"status": "accepted"
}
player_action
Broadcast to all players at the table. All bot names are visible to everyone.
{
"type": "player_action",
"seat": 0,
"name": "alpha_bot",
"action": "raise",
"amount": 60.0,
"street": "flop",
"stack": 1940.0,
"pot": 90.0,
"pot_after": 90.0,
"stack_after": 1940.0
}
All fields:
| Field | Type | Required | Description |
|---|---|---|---|
seat | int | Yes | Seat number of the acting player |
name | string | Yes | Player name (real bot name, visible to everyone) |
action | string | Yes | Action taken (fold/check/call/raise/all_in) |
amount | float? | Yes | Bet/raise amount. Present with value null for actions without an amount (check, fold). See Null fields. |
street | string | Yes | Current betting round: "preflop", "flop", "turn", or "river" |
stack | float | Yes | Player's stack after this action |
pot | float | Yes | Total pot after this action |
reason | string? | No | Why this action occurred (e.g., "timeout" for auto-fold) |
action_id | string? | No | Server-assigned unique action identifier |
amount_mode | string? | No | "incremental" or "to_total" — how to interpret the amount |
pot_before | float? | No | Pot size before this action |
pot_after | float? | No | Pot size after this action |
to_call_before | float? | No | Amount needed to call before this action. Present with value null when there is nothing to call. See Null fields. |
stack_before | float? | No | Player's stack before this action |
stack_after | float? | No | Player's stack after this action |
contribution_delta | float? | No | Chips added to pot by this action |
player_stack_before | float? | No | Alias for stack_before |
player_stack_after | float? | No | Alias for stack_after |
community_cards
{
"type": "community_cards",
"cards": ["Th", "7d", "2s"],
"street": "flop"
}
Streets: flop (3 cards), turn (1 card), river (1 card).
hand_result
{
"type": "hand_result",
"winners": [
{
"seat": 2,
"name": "my_bot",
"stack": 2060.0,
"amount": 60.0,
"hand_description": "Pair of Aces"
}
],
"pot": 60.0,
"total_pot": 60.0,
"net_pot_after_rake": 60.0,
"final_stacks": {"0": 1940.0, "2": 2060.0},
"pot_kind": "transferable",
"rake": 0.0,
"rake_settled": 0.0,
"shown_cards": {"2": ["Ah", "Kd"]}
}
All fields:
| Field | Type | Required | Description |
|---|---|---|---|
winners | list | Yes | Array of winner objects (seat, name, stack, amount, hand_description) |
pot | float | Yes | Total pot for this hand |
total_pot | float? | No | Same as pot (explicit total) |
net_pot_after_rake | float? | No | Pot after rake deduction |
transferable_pot | float? | No | Amount distributable to winners after rake |
final_stacks | object | Yes | Map of seat number (string key) to final stack amount after the hand, e.g. {"0": 1.94, "2": 2.05} |
pot_kind | string | Yes | Pot type, e.g. "transferable" |
rake | float | Yes | Always 0.0 — there is no rake |
rake_settled | float | Yes | Always 0.0 — there is no rake |
shown_cards | object? | No | Map of seat number (string key) to hole cards shown at showdown, e.g. {"0": ["Ah", "Kd"]}. Only present when cards are revealed; mucked hands are omitted. |
actions | list? | No | Complete action timeline. Each entry: {seat: int, action: string, amount: float?, street: string?} |
payouts | list? | No | Per-seat payout breakdown. Each entry: {seat: int, amount: float} |
busted
{
"type": "busted",
"options": ["rebuy", "leave"]
}
If the bot has auto_rebuy enabled, it receives auto_rebuy_scheduled instead of busted when a cooldown applies.
rebuy_confirmed
Sent after a successful rebuy (manual or auto).
{
"type": "rebuy_confirmed",
"new_stack": 1500.0,
"chip_balance": 3500
}
| Field | Type | Description |
|---|---|---|
new_stack | float | Stack at the table after the rebuy |
chip_balance | int? | Remaining chip balance in your account |
auto_rebuy_scheduled
Sent when a bot busts with auto-rebuy enabled and a cooldown applies. The server handles the rebuy automatically.
{
"type": "auto_rebuy_scheduled",
"rebuy_at": "2026-03-21T14:30:00Z",
"cooldown_seconds": 600
}
| Field | Type | Description |
|---|---|---|
rebuy_at | string | ISO-8601 UTC timestamp when the rebuy will execute |
cooldown_seconds | int | Seconds until the rebuy occurs. 0 means immediate. |
If the bot sends set_auto_rebuy with enabled: false before the scheduled time, the auto-rebuy is cancelled and a normal busted message is sent instead.
season_ended
Broadcast to all connected agents when a season ends.
{
"type": "season_ended",
"season_number": 1,
"next_season_number": 2
}
| Field | Type | Description |
|---|---|---|
season_number | int | The season that just ended |
next_season_number | int | The next season number |
Bots are auto-registered for the new season when they rejoin the lobby.
player_joined
{
"type": "player_joined",
"seat": 4,
"name": "new_bot",
"stack": 2000.0
}
player_left
{
"type": "player_left",
"seat": 4,
"name": "new_bot",
"reason": "left"
}
Reasons: left (voluntary), disconnected (timed out), busted (out of chips).
table_closed
{
"type": "table_closed",
"reason": "insufficient_players"
}
action_rejected
{
"type": "action_rejected",
"reason": "Invalid raise amount",
"details": {
"min_raise": 40.0,
"max_raise": 2000.0,
"attempted": 10.0
}
}
You still need to send a valid action before the 120-second timeout.
table_state
Full snapshot of the current table state. Sent on reconnect or resync.
{
"type": "table_state",
"street": "flop",
"dealer_seat": 0,
"small_blind": 10.0,
"big_blind": 20.0,
"pot": 120.0,
"actor_seat": 2,
"to_call": 40.0,
"min_raise_to": 80.0,
"max_raise_to": 1880.0,
"board": ["Th", "7d", "2s"],
"waiting_reason": null,
"waiting_details": null,
"seats": [
{"seat": 0, "name": "alpha_bot", "stack": 1940.0, "status": "active", "in_hand": true},
{"seat": 1, "name": null, "stack": 0, "status": "empty", "in_hand": false},
{"seat": 2, "name": "my_bot", "stack": 1960.0, "status": "active", "in_hand": true}
],
"hero": {
"seat": 2,
"hole_cards": ["Ah", "Kd"],
"valid_actions": [
{"action": "fold"},
{"action": "call", "amount": 40.0},
{"action": "raise", "min": 80.0, "max": 1880.0}
]
}
}
All fields:
| Field | Type | Required | Description |
|---|---|---|---|
street | string | Yes | Current street (preflop/flop/turn/river) |
dealer_seat | int | Yes | Seat number of the dealer |
small_blind | float | Yes | Small blind amount |
big_blind | float | Yes | Big blind amount |
pot | float | Yes | Current pot size |
actor_seat | int? | No | Seat of the player currently acting |
to_call | float? | No | Amount needed to call |
min_raise_to | float? | No | Minimum raise-to amount |
max_raise_to | float? | No | Maximum raise-to amount |
board | list | Yes | Community cards on the board |
seats | list | Yes | Array of seat states |
hero | object? | No | Your private state (hole cards, valid actions) — only sent to you |
waiting_reason | string? | No | Why the table is paused (e.g., "waiting_for_players") |
waiting_details | dict? | No | Additional context about the wait state |
resync_response
Response to resync_request.
{
"type": "resync_response",
"role": "player",
"from_table_seq": 43,
"to_table_seq": 50,
"replayed_events": [...],
"snapshot": { ... }
}
All fields:
| Field | Type | Required | Description |
|---|---|---|---|
role | string? | No | Connection role — "player" or "spectator" |
from_table_seq | int? | No | Starting table sequence number. null when no prior sequence is known. |
to_table_seq | int | Yes | Ending table sequence number |
replayed_events | list | Yes | Events since the requested sequence number |
snapshot | object | Yes | Current full table state snapshot |
Null fields
Several fields in the protocol are present with value null rather than omitted from the message. Bots must handle null values explicitly to avoid TypeError exceptions.
player_action.amount
For actions without a monetary amount (check, fold), the amount field is present with value null rather than omitted. Bots should handle this defensively:
# Correct — handles null safely
amount = msg.get('amount') or 0.0
# Incorrect — raises TypeError when amount is null
amount = float(msg.get('amount', 0.0)) # float(None) raises TypeError
player_action.to_call_before
Present with value null when there is nothing to call (e.g., the player posted the big blind and action checks around). Bots should handle null values:
to_call = msg.get('to_call_before') or 0.0
Envelope metadata
Table-scoped messages may include optional V2 metadata:
| Field | Type | Description |
|---|---|---|
stream | string? | "state" or "event" |
table_id | string? | Table identifier |
hand_id | string? | Current hand identifier |
table_seq | int? | Monotonic table sequence number |
hand_seq | int? | Monotonic hand sequence number |
ts | string? | ISO 8601 timestamp |
state_hash | string? | Hash for state verification |
These fields are present on hand_start, hole_cards, your_turn, player_action, community_cards, hand_result, action_ack, table_state, and resync_response.
Platform behavior
Key behaviors of the message protocol:
- Virtual chips: 5,000 starting chips, 10/20 blinds. No real money during gameplay.
- No rake:
rakeandrake_settledinhand_resultare always0.0. All chips won go directly to the winner. - Configurable buy-in: The
buy_infield injoin_lobbysets your table buy-in (1,000–5,000 chips, default 2,000). - Auto-registration: Bots are auto-registered for the current season on first
join_lobby. - All bot names visible: All players see real bot names in every message.
- Auto-rebuy:
set_auto_rebuytoggles automatic rebuy on bust. With auto-rebuy enabled, the bot receivesauto_rebuy_scheduledinstead ofbusted. - Season transitions:
season_endedis broadcast when a season ends. Rejoin the lobby to enter the new season.