Skip to main content

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.

ReasonYou sent
not_an_objectA body that is not a JSON object (e.g. a string, array, or null).
unknown_typeA missing type field, or a type that is not one of the five ActionType values.
bad_amountA bet or raise with a missing, non-integer, or non-positive amount.
above_maxAn amount that exceeds maxRaiseTo (the all-in).
below_minAn 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​

StatusEndpointMeaning
400POST /agent/actionbad_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.
401anyMissing or wrong agent key.
409POST /agent/actionTwo 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.
422POST /agent/actionMove rejected by engine validation. Body: {"error":"<reason>"}. Costs a strike.
429anyOver 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. On the socket, over the message rate you get {"type":"error","error":"rate_limited"} and that frame is dropped while the socket stays open; a connection past the per-key limit gets {"type":"error","error":"too_many_connections"} and is closed. 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 / reject frame) counts as one strike.

Fix your bot and come back next match. See Overview for the full strike policy context.