Reconnection & Idempotency
Use bounded exponential backoff with jitter for reconnects. The server holds a disconnected seat for 120 seconds, but that is a recovery window, not a delay target.
A transport disconnect does not immediately fold a pending hand. The original action deadline keeps running. If it expires first, the server auto-folds (or auto-checks when folding is not legal); reconnecting never resets that deadline.
Warm reconnect
Section titled “Warm reconnect”If the process retained table_id and its highest applied table_seq:
- Reconnect with the same API key and wait for
connected. - Send
resync_requestwith the retained table and sequence. - Apply the response using the state-consistency rules.
- If the player snapshot contains both
hero.valid_actionsandhero.turn_token, act using that snapshot’shand_id, actions, and token. The existing deadline is unchanged. If there is no token, wait foryour_turn.
{ "type": "resync_request", "table_id": "t-abc123", "last_table_seq": 42}Cold restart
Section titled “Cold restart”A restarted process may know neither the table nor a sequence number.
- Before joining the lobby, call
GET /api/me/active-gamewith the same Bearer key. - If
playingistrue, connect the WebSocket and sendresync_requestwith the returnedtable_id. Uselast_table_seq: 0to request the server’s currently retained public replay window, or omit it when only the current snapshot is needed. - If
playingisfalse, connect and sendjoin_lobbyonce. - If a race returns
error.code: "already_seated", readtable_idandseatfrom that error, stop sendingjoin_lobby, and request resync for that table.
GET /api/me/active-game returns:
{ "playing": true, "table_id": "t-abc123", "seat": 2, "stack_chips": 1960}The replay buffer is bounded and excludes private/direct-only messages such as
hole_cards, your_turn, action_ack, table_state, and prior
resync_response objects. The player snapshot restores current private hero state;
it does not provide a complete historical log.
Pending turn and spectator examples
Section titled “Pending turn and spectator examples”These are abbreviated to show role-specific behavior. When the resyncing player is the current actor, the snapshot restores the existing turn token without restarting or extending the turn timer:
{ "type": "resync_response", "role": "player", "to_table_seq": 50, "snapshot": { "type": "table_state", "hand_id": "h-xyz789", "actor_seat": 2, "hero": { "seat": 2, "hole_cards": ["Ah", "Kd"], "valid_actions": [{"action": "call", "amount": 40}], "turn_token": "current-token" } }}A spectator response omits snapshot.hero entirely:
{ "type": "resync_response", "role": "spectator", "to_table_seq": 50, "snapshot": { "type": "table_state", "hand_id": "h-xyz789", "actor_seat": 2 }}The complete table_state fields omitted above are still required on the wire; see
the message catalog.
Action retry rules
Section titled “Action retry rules”Generate a unique client_action_id for each decision and store the exact payload
until it is resolved.
- A successful action produces
action_ackwith the sameclient_action_id. - The matching
player_action.action_idcorrelates the table broadcast. - Retrying the same ID with the identical payload replays the cached acknowledgement without applying the action twice.
- Reusing the ID with a different payload returns top-level
code: "action_id_conflict". - Never change or reuse
hand_idorturn_token. A new action-authority message (your_turn, or an acting-player resync snapshot) supersedes the old decision.
The actor currently receives action_ack before the corresponding
player_action, followed by state updates. Not every recipient receives every one of
those messages, and unrelated table messages may appear between them. Correlate by
client_action_id/action_id, hand_id, and monotonic sequence watermarks rather
than assuming adjacent messages.
Recovery loop guard
Section titled “Recovery loop guard”Do not alternate indefinitely between join_lobby and resync. Once either
active-game or already_seated supplies a table ID, remain in recovery mode for
that table until one of these occurs:
- a valid player
resync_responsearrives; table_closedarrives;- the server reports
table_not_foundornot_at_table; - the 120-second seat window expires and
active-gamereportsplaying: false.