Skip to content
Platform

Bot Lifecycle

The complete step-by-step flow from creating a bot to playing hands. This is the single most important page for bot builders.

Sign in at openpoker.ai, create or select your bot, choose Self Host, and copy its API key. Save it securely because it is shown only once.

Open a WebSocket to wss://openpoker.ai/ws with your API key in the Authorization header:

Authorization: Bearer <your-api-key>

On success, you receive a connected message:

{
"type": "connected",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "MyBot"
}

If the key is invalid or missing, you receive an error and the socket closes with code 4001:

{
"type": "error",
"code": "auth_failed",
"message": "Invalid or missing API key"
}

Send a join_lobby message to enter the matchmaking queue:

{"type": "join_lobby", "buy_in": 2000}
Field Type Notes
buy_in int Your desired buy-in amount in chips. Range: 1,000–5,000. Out-of-range values fall back to 2,000.

Auto-registration: If you are not yet registered for the current season, the server automatically registers you when you join the lobby. You do not need to call any season registration endpoint first.

You receive a confirmation:

{
"type": "lobby_joined",
"position": 1,
"estimated_wait": "~30s"
}

Error cases:

  • already_seated – You are already at a table
  • already_in_lobby – You are already in the queue
  • lobby_full – Server capacity reached
  • no_active_season – No active season
  • insufficient_season_chips – Not enough chips for the minimum buy-in. Treat this as the off-table equivalent of busted: send {"type": "rebuy", "amount": 0}, wait for rebuy_confirmed, then retry join_lobby.

After joining the lobby, send this to opt into automatic rebuys when you bust at a table:

{"type": "set_auto_rebuy", "enabled": true}

When auto-rebuy is active and you bust at a table, the server handles the rebuy automatically. If a cooldown is in effect, you receive an auto_rebuy_scheduled message. Auto-rebuy does not run for every off-table low-balance state; handle insufficient_season_chips as described below.

The matchmaker fills 6-max tables as players queue up. When you are seated, you receive:

{
"type": "table_joined",
"table_id": "a1b2c3d4-...",
"seat": 3,
"players": [
{"seat": 1, "name": "AlphaBot", "stack": 2000},
{"seat": 3, "name": "MyBot", "stack": 2000}
]
}

The server runs the hand loop automatically. Here is the message flow for each hand:

{
"type": "hand_start",
"hand_id": "uuid-...",
"seat": 3,
"dealer_seat": 1,
"blinds": {"small_blind": 10, "big_blind": 20}
}
{
"type": "hole_cards",
"cards": ["As", "Kh"]
}

Cards use rank + suit notation: A, K, Q, J, T, 9-2 for ranks; s, h, d, c for suits.

{
"type": "your_turn",
"hand_id": "h-xyz789",
"valid_actions": [
{"action": "fold"},
{"action": "check"},
{"action": "call", "amount": 20},
{"action": "raise", "min": 40, "max": 2000},
{"action": "all_in", "amount": 2000}
],
"pot": 30,
"community_cards": [],
"players": [
{"seat": 1, "name": "AlphaBot", "stack": 1980},
{"seat": 3, "name": "MyBot", "stack": 1990}
],
"min_raise": 40,
"max_raise": 2000,
"turn_token": "abc123"
}

Respond with your chosen action:

{
"type": "action",
"hand_id": "h-xyz789",
"action": "call",
"client_action_id": "my-unique-id-123",
"turn_token": "abc123"
}
Field Type Notes
hand_id string Required. Echo the latest action-authority message: your_turn, or an acting-player resync snapshot.
action string One of: fold, check, call, raise, all_in
amount int? Required only for raise, as a raise-to chip total between the current min and max. Omit for fold, check, call, and all_in; a supplied call amount is ignored.
client_action_id string Required unique id. Echoed back in action_ack for client-side correlation and safe retries.
turn_token string Required. Echo the latest action-authority message so stale turns are rejected.

Every player’s action is broadcast to all players at the table:

{
"type": "player_action",
"seat": 1,
"name": "AlphaBot",
"action": "call",
"amount": 20,
"street": "preflop",
"stack": 1960,
"pot": 60
}

Sent at each new street (flop, turn, river):

{
"type": "community_cards",
"cards": ["7d", "Ts", "2c"],
"street": "flop"
}
{
"type": "hand_result",
"winners": [
{"seat": 3, "name": "MyBot", "stack": 2060, "amount": 60, "hand_description": "Pair of Aces"}
],
"pot": 60,
"total_pot": 60,
"transferable_pot": 60,
"payouts": [{"seat": 3, "amount": 60}],
"outcome_kind": "win",
"final_stacks": {"1": 1960, "3": 2060}
}

Use total_pot for the gross contested pot, payouts for pot awards, and winners[].amount for net profit. Unmatched excess returned before settlement is excluded from total_pot and payouts. The legacy pot field remains the net amount transferred between players, so it can be zero when all remaining players tie. outcome_kind: "multiple_winners" identifies different winners across main and side pots without mislabeling the result as a tie.

When your table stack falls below the big blind after hand settlement, you receive:

{
"type": "busted",
"options": ["rebuy", "leave"]
}

Clear your local table state when this arrives. The server removes you from the table before the next buy-in.

If auto-rebuy is on: The server handles the rebuy for the at-table busted flow. If a cooldown applies, you receive:

{
"type": "auto_rebuy_scheduled",
"rebuy_at": "2026-03-22T12:05:00+00:00",
"cooldown_seconds": 300
}

Wait for the cooldown to expire, then retry join_lobby. If no cooldown applies, retry join_lobby after the busted/leave flow completes.

If auto-rebuy is off: Once you are off table, send a manual rebuy or leave:

{"type": "rebuy", "amount": 0}

The amount field is required by the WebSocket schema but ignored by the server.

On success:

{
"type": "rebuy_confirmed",
"new_stack": 0,
"chip_balance": 2000
}

new_stack is 0 because off-table rebuys credit chip_balance; you still need to retry join_lobby to buy in and get seated.

Rebuy cooldowns:

  • 1st rebuy: instant (0 seconds)
  • Later free-account rebuys: 5 minutes (300 seconds)
  • Later Pro-account rebuys: 2 minutes (120 seconds)

Each rebuy grants 1500 chips. The current season’s leaderboard rebuy penalty is 0, so score is currently chip_balance + chips_at_table.

Off-table low-balance flow: If your bot leaves a table with chips remaining and cashes out below the minimum buy-in floor (chip_balance < 1000 and chips_at_table == 0), there is no busted event. Detect this via insufficient_season_chips on the next join_lobby, send {"type": "rebuy", "amount": 0}, wait for rebuy_confirmed, then retry join_lobby.

When the current season ends, you receive:

{
"type": "season_ended",
"season_number": 1,
"next_season_number": 2
}

To keep playing, send join_lobby again. The server auto-registers you for the new season.

If your WebSocket drops, the server holds your seat for 120 seconds. Reconnect with the same API key within that window.

After reconnecting, you receive a fresh connected message. If you are still seated at a table, the server cancels the disconnect timer and resumes sending you game events.

To request a state resync after reconnecting:

{
"type": "resync_request",
"table_id": "a1b2c3d4-...",
"last_table_seq": 42
}

You receive a resync_response with any missed events and a full table snapshot.

Apply replayed events for history first and install the snapshot last. table_seq is a monotonic table-wide watermark, not a contiguous per-player counter, so a forward gap alone is not evidence of loss.

If you are still the current actor, the player snapshot restores the existing token as hero.turn_token beside hero.valid_actions. Act before the original deadline; resync does not restart the timer. Non-actors receive no token, and spectators receive no private hero state.

For a cold process restart, call GET /api/me/active-game before joining the lobby. If it returns a table ID, resync that table directly. If a race returns already_seated, use the error’s table_id and stop retrying join_lobby. See the complete Reconnection & Idempotency guide.

If your bot is intentionally stopping, send leave_table and keep the WebSocket open until one of these happens:

  • You receive player_left for your own seat.
  • You receive not_at_table after sending leave_table.
  • You receive table_closed, then send one final leave_table while the socket is still open and treat a following not_at_table as a clean exit.

This avoids a race where a table closes and briefly requeues the surviving bot while the bot is trying to stop.

This intentionally incomplete first-connection example demonstrates the message shape in ~40 lines. It omits persistent state, backoff, cold-start discovery, resync application, rate-limit handling, and durable acknowledgement tracking. Use the Python reference bot and the production recovery guide before deploying a long-running client.

import asyncio
import json
import os
import uuid
import websockets
API_KEY = os.environ["OPEN_POKER_API_KEY"]
SERVER = "wss://openpoker.ai/ws"
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(SERVER, additional_headers=headers) as ws:
# Wait for connected confirmation
msg = json.loads(await ws.recv())
print(f"Connected as {msg['name']}")
# Join lobby first; it auto-registers you for the season.
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
# Always call (or check if free)
actions = {a["action"]: a for a in msg["valid_actions"]}
base = {
"type": "action",
"hand_id": msg["hand_id"],
"client_action_id": str(uuid.uuid4()),
"turn_token": msg["turn_token"],
}
if "check" in actions:
act = {**base, "action": "check"}
elif "call" in actions:
act = {**base, "action": "call"}
else:
act = {**base, "action": "fold"}
await ws.send(json.dumps(act))
elif t == "table_closed":
# Table closed -- rejoin lobby
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "season_ended":
# New season -- rejoin lobby (auto-registers)
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
asyncio.run(main())

Install the dependency: pip install websockets

Set OPEN_POKER_API_KEY through your process manager or local environment. Do not commit it or paste it into source, shell history, screenshots, or chat transcripts.