code you do not recognise, treat it as a generic failure of its
HTTP status class. New codes get added; that is why matching on the class is safe
and matching on the exact string is not.
Codes by status
400 — the request is malformed or self-contradictory
Never retry unchanged. The message names what would have worked.invalid_parameter is the query string; invalid_request_body is the
JSON body. The distinction is deliberate: being told your body is bad when
the fault is in the query sends you looking in the wrong place.401 — we do not know who you are
Retrying with the same credential will not help. A bearer failure also carries a
WWW-Authenticate challenge; an API-key failure does not. See
Authentication.
403 — we know who you are, and you may not do this
404 — not there, or not yours
order_not_found deliberately covers “not yours” as well as “does not exist.”
Distinguishing them would let anyone probe which order ids exist on the venue.409 — the venue is not accepting this right now
A venue-wide stop is
409. A single market being halted is 422 market_halted.
Two different statuses because the recoveries differ — one market halting means
trade elsewhere; the venue halting means stop.
422 — well-formed, but a pre-trade check rejected it
Retrying the identical request will fail identically.429 — slow down. Nothing was sequenced
Both are safe to retry: nothing reached the matching engine, so no order exists.
Back off, then retry.
500 / 503 — our side
engine_unavailable and result_unknown are both 503 and they need opposite
handling. Branch on the code, not the status.
result_unknown
This is the one that punishes a naive client.result_unknown means the command was submitted to the matching engine but no
reply arrived before the deadline. It may have committed. Your order may be
resting, may be filled, or may never have existed.
It is not a success and it is not a failure. Two things you must not do:
- Do not retry with a new
client_order_id. If the first command committed, you now hold twice the position you intended. This is the single most expensive mistake a trading bot can make against this API. - Do not treat it as a rejection. Your position tracking will drift from reality and stay drifted.
client_order_id, same everything:
client_order_id, so a
re-send that arrives after the first command committed returns the existing
order instead of creating a second one. This is what makes the retry safe, and it
only works if the id is identical.
insufficient_shares
Worth its own section because it surprises nearly everyone. There are no short positions. Asell for shares you do not hold is
422 insufficient_shares — not a short, not a naked position, a rejection.
To express a bearish view you buy the other outcome: buy no rather than
sell yes. See Binary markets, which also explains why
those two are not the same order despite looking like they should be.
The same rule applies per rung of a survival ladder.
Rate limits
One token bucket per credential across the whole surface: 250 requests/second sustained, 500 burst. A key gets its own bucket; a bearer token draws on one keyed to the granting application and user.
A
401 carries none of them: the bucket is keyed on the resolved credential, so a
request that fails authentication is rejected before the limiter runs.