Skip to main content
Every failure returns the same JSON shape:
Branch on code, never on error.code is a closed vocabulary and part of the contract. error is free text for a human reading a log — it may be reworded at any time, without notice and without a version bump. Any client that pattern-matches the message will break on a copy edit.
If you receive a 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.
Two safe recoveries. Either works; pick one and be consistent. Poll the order. You know the order id from the response body when one was returned, or from a listing:
Or re-send the identical request — same client_order_id, same everything:
The engine deduplicates on the identity derived from your 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. A sell 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.
Self-pace from X-RateLimit-Remaining instead of discovering the cap by hitting it. A throttled cancel is the dangerous one — it leaves an order resting that you believe you pulled, and your next decision is made against a position you do not actually have.