Poker Panel Developer API
Quickstart
curl https://pokerpanel.app/v1 # public discovery (try it now)
curl -H "Authorization: Bearer $POKERPANEL_API_KEY" \
https://pokerpanel.app/v1/players
Base URL https://pokerpanel.app/v1 · REST auth Authorization: Bearer · WebSocket auth ?key= · CORS on everything · 240 req/min + 5 sockets per key.
Endpoints
| Endpoint | Returns |
|---|---|
GET /v1 · /v1/key | Discovery · which venue this key belongs to |
GET /v1/venue | Venue, live flag, current game & stakes |
GET /v1/live/state | Table now: seats, stacks, board, pot, action |
WS /v1/live/events?key=…&since_seq=N | Real-time events, with catch-up for a dropped socket (below) |
GET /v1/players · /v1/players/{id} | Roster · profile + lifetime VPIP, PFR, 3-bet, AF, WTSD, W$SD, BB/100 |
GET /v1/leaderboard?period=YYYY-MM | Monthly or all-time (omit period) |
GET /v1/sessions · /v1/sessions/{uid} | Nights of poker |
GET /v1/hands?page=N · /v1/hands/{id} | Hand summaries · full action-by-action replay |
GET /v1/hands?since=TS · ?after_id=ID | Everything after a cursor, oldest-first, for incremental sync |
POST /v1/transactions/buy-in · /cash-out | Record money on the room's ledger (below) |
GET/POST /v1/webhooks · DELETE /v1/webhooks/{id} | HTTPS callbacks (hand.finished, payout.applied, transaction.applied), HMAC-signed |
Historical endpoints serve 24/7 even when the venue's Mac is off. player_id is a permanent UUID (survives renames and merges; safe to cache). v1 only changes additively: ignore unknown fields and event kinds and your integration keeps working.
Two planes, two cursors. History refreshes when a night ends, so tonight's hands are on the socket and finished nights are on /v1/hands. Sync history with ?since= or ?after_id= and never with page numbers: pages are newest-first, so every publish renumbers them and a remembered page number reads a different set of hands next time. The cursor response is oldest-first and hands back a cursor to send next time.
The event stream
On connect: one state frame, then events. Kinds: start_hand · player_action · deal_street · street_advance · end_hand · payout_applied · transaction_applied. Ordered by seq.
{"type":"event","seq":4182,"ts":1786500000,"kind":"player_action",
"payload":{"hand_number":214,"seat_id":3,"action_type":"raise","amount":1200,
"street":"turn","player_id":"8f3c…","name":"Marcus Webb","decision_ms":8400}}
Each event is self-contained. start_hand carries the seats that were dealt in, with the stack each started with and what they posted — don't reconstruct that from the state frame that follows it, which arrives after the event and is already net of the blinds. end_hand carries the hand's money: per-seat contributed, won and net, plus the board, the street it ended on and the rake. payout_applied carries the stack either side of each credit. transaction_applied is a stack change that is money, not poker — a buy-in, a cash-out, or an operator correcting a stack — with a signed delta, so a ledger can sum the stream instead of guessing which stack moves were pots.
decision_ms is pace, not a shot clock. It is the gap since the previous commit at that table — the deal, the new street, or the last action. The stamp is when the action reached the engine, so at an operator-driven table it carries the operator's entry cadence too. It is null for any gap over five minutes, because a chip count or a floor call is not a decision. Fine for "this table plays fast"; not evidence that a player tanked.
Catching up after a drop. The state frame heals table state — pot, board, seats — but it cannot tell you which actions you missed. Reconnect with ?since_seq= set to the last seq you processed and a replay envelope arrives after the state frame, followed by the buffered events in order.
{"type":"replay","since_seq":4180,"count":2,"complete":true,
"reset":false,"oldest_seq":3990,"newest_seq":4182,"window":250}
Read complete and believe it. It is true only when the buffer can prove nothing was dropped after your cursor; false means there is a gap, and the fix is to reconcile against /v1/hands rather than assume you are current. reset:true means the venue's engine restarted and its counter began again, so your cursor belongs to a previous run: the whole window is replayed and you deduplicate on hand_number. The buffer is a short in-memory window, not durable history.
Integrity rules
- No live hole cards, ever. Enforced by a server-side whitelist, not a setting.
- Hand histories carry shown-at-showdown cards only; mucked hands have no hole_cards field.
- Money display is venue policy: big blinds (default), dollars, or hidden.
- Venues control the roster: which players appear and under what name.
- Real hands only; test and simulated deals never enter the API.
- Nothing folds, calls, raises or deals through this API, and that isn't a read-only stance — buy-in and cash-out move real money. The line is whether your client can be right about the state it's acting on, which is the same reason a buy-in is refused mid-hand. A buy-in is between hands, idempotent and matches a physical act at the cage; a fold is time-critical, not usefully idempotent once action has moved, and invisible to everyone until the hand is over. If you need player-driven actions to reach a table, ask us about an action-intent channel where the engine stays the authority and refuses anything that doesn't match the hand and seat you believed you were acting on.
Networks of card rooms
The API is per-venue by design: one key per room, identical endpoints at every room. A network app is a loop over its keys. GET /v1/key tells you which room a key belongs to, and each room's operator can issue or revoke your key from their own rig, instantly. Aggregate leaderboards, cross-room player apps, and network dashboards are all client-side joins on player_id + venue.
Writing to the ledger
POST /v1/transactions/buy-in and /cash-out record money against a player. Your key already does this — a room has one key and it does everything, so treat it as a credential that can move money rather than a read token. The room can revoke it instantly.
curl -X POST https://pokerpanel.app/v1/transactions/buy-in \
-H "Authorization: Bearer $POKERPANEL_API_KEY" \
-d '{"player_name":"Marcus Webb","amount":200,
"idempotency_key":"buyin-2026-09-10-0001"}'
The idempotency key is required, and reusing it on a retry is the point. The engine runs a live conservation guard, so a movement applied twice doesn't leave a duplicate row — it breaks the table mid-hand. A settled answer, refusals included, is replayed verbatim with Idempotent-Replay: true. A 5xx is deliberately not remembered, because 504 host_timeout doesn't mean it didn't happen: retry with the same key.
Two refusals worth designing around. 409 hand_in_progress — money moves between hands; the engine lets an operator standing at the table take an emergency buy-in mid-hand, and this API doesn't, because your client can't see the felt. 503 host_offline — reads serve from the relay whether or not the room's Mac is awake, and writes need it. 60 writes per minute, on their own budget.
What changed
One brief, not two. agents.txt is always the complete current spec, so a new integration reads it start to finish and an existing one reads this section. Its own CHANGELOG carries the same entries with the detail. GET /v1 reports updated and changelog, so an integration can check for drift without fetching anything else.
2026-09-16
- Corrected — payout_applied. This page and the brief both described it as per-seat stack deltas. It never carried any; the shape was {hand_number, winner_seat, amount}. It carries them now: stack_before, stack_after, delta, player_id. If you wrote against the documented shape you have been reading undefined fields.
- Added — start_hand carries the dealt-in seats, with the stack each started the hand with and what they posted. Reconstructing this from the following state frame gave you a small blind one blind short.
- Added — end_hand carries the hand's money: per-seat contributed/won/net, the board, the street and the rake. Per-hand results no longer wait for the venue's bundle to refresh at session close.
- Added — event kind transaction_applied and webhook transaction.applied: buy-in, cash-out, or an operator correcting a stack. Until now, a stack that grew because money arrived and a stack that won a pot were the same thing on the wire.
- Added — decision_ms on player_action, and ts + decision_ms on every action in hand detail. Read the caveat above before showing it to anyone.
2026-09-10
- Added — the API is no longer read-only. POST /v1/transactions/buy-in and /cash-out. Your existing key already carries this; there is no separate write key to request, which also means the key you hold can move money.
- Corrected — the event stream. The brief used to say a dropped socket is healed by the next state frame and told you not to build replay logic. That was wrong: the state frame heals table state and says nothing about which actions you missed. Reconnect with ?since_seq= and read complete on the replay envelope. If you built against the old advice, you are losing hands on every drop and cannot see it.
- Added — cursors on /v1/hands: ?since= and ?after_id=. Syncing by page number has a quieter version of the same bug, since pages are newest-first and renumber on every publish.
- Added — webhook kind payout.applied, once per winning seat. A hook with no kinds filter receives every kind, so switch on kind in the body.
- Changed — POST /v1/webhooks refuses an unknown kinds entry with 400 unknown_kind instead of accepting it and never delivering.
Get a key
Ask the card room. Venues on the Card Room plan issue keys directly from Poker Panel, no middleman. Building something bigger, or want a sandbox venue? Email with what you're building.
[email protected]