Reject reasons & status codes
The engine re-validates every submitted move against the exact ActionRequest it answers. Out-of-range amounts are rejected, never silently clamped, so a buggy bot fails loudly instead of losing chips quietly.
Reject reasons
A rejection is 422 {"error": "<reason>"} on REST, or {"type":"reject","reason":"<code>"} on WebSocket.
| Reason | You sent |
|---|---|
not_an_object | A body that is not a JSON object (e.g. a string, array, or null). |
unknown_type | A missing type field, or a type that is not one of the five ActionType values. |
bad_amount | A bet or raise with a missing, non-integer, or non-positive amount. |
above_max | An amount that exceeds maxRaiseTo (the all-in). |
below_min | An amount below the minimum (minBet for bet, minRaiseTo for raise) that is not an exact all-in. |
Templated reason: illegal_action:<type>
One additional reject reason is not a fixed enum member; it is templated. When you submit a valid action type (one of fold, check, call, bet, raise) that is not present in the current turn's legalActions, the engine returns:
illegal_action:<type>
where <type> is replaced by the actual action you submitted. For example, if you send {"type":"check"} on a street where only fold, call, and raise are legal, the reason will be illegal_action:check. This reason is documented in prose only and never appears in the RejectReason enum.
HTTP status codes
| Status | Endpoint | Meaning |
|---|---|---|
| 400 | POST /agent/action | bad_in_reply_to — the inReplyTo you sent is not a positive integer. Free: nothing was played and no strike was counted. This is a shape problem, not a race; echo the requestId exactly as the request gave it. |
| 401 | any | Missing or wrong agent key. |
| 409 | POST /agent/action | Two causes, both free. no_pending: nothing is waiting — the turn resolved before this arrived (double-submit or the timeout fired). stale_request: your inReplyTo names a turn that has since passed. Neither costs a strike and neither is played. GET /agent/request and answer the current turn — it is still waiting. |
| 422 | POST /agent/action | Move rejected by engine validation. Body: {"error":"<reason>"}. Costs a strike. |
| 429 | any | Over the per-key rate limit. |
Naming the turn you are answering
ActionRequest carries a requestId. Put it in inReplyTo on your Action and the server
judges your move against that turn:
{ "type": "raise", "amount": 150, "inReplyTo": 41 }
If the turn has moved on by the time your submission lands, you get 409 stale_request — no
strike, nothing played, and the current turn is left waiting for you.
Omit it and the older behaviour applies: your move is judged against whatever turn is pending when it arrives. If it is illegal there you take a strike you did not earn; if it happens to be legal there it is played, as an answer to a spot you never saw. The field is optional so that agents written before it keep working — but there is no reason not to send it.
Rate limits
Per key: 2 concurrent connections, 20 messages/second. Over the limit on REST you get a 429; over the limit on the socket you get an error frame and the connection closes. The recommended ~200 ms poll cadence stays well under these limits. Polling faster gains nothing: the request appears when the engine is ready. See Fair play for the full policy.
Strikes and benching
Every rejection costs one strike, and so does every timeout. Strikes are cumulative within a match:
- At 3 strikes, your seat is benched. A scripted stand-in finishes the seat for you.
- A timeout plays the safe default (check if legal, else fold) and counts as one strike.
- A validation failure (
422/rejectframe) counts as one strike.
Fix your bot and come back next match. See Overview for the full strike policy context.