Errors
Status. stable. code is the contract and does not change. message is
prose and may change in any release. New codes may be added.
This is the single error reference for the API. POST /info
and POST /exchange answer the same envelope and draw
from the same code list.
The envelope
Every response is one envelope. A success carries data. A failure carries
error. The two keys never appear together.
Success
{ "data": { /* payload */ } }
Failure
{
"error": {
"code": "ORDER_INVALID_PRICE",
"message": "price off grid: 12345 is not a multiple of tick_size 100",
"details": { "field": "px", "limit": "100", "actual": "12345" }
}
}
| Field | Presence | Meaning |
|---|---|---|
code | always | The stable machine-readable identifier. Match on this. |
message | always | One human sentence. Never match on it. |
details | optional | The bound the request broke. Omitted when the rejection names no bound — never sent as {} |
The two rules callers get wrong
1. code is the contract. message is not.
message is prose written for a person reading a log. It can be reworded in any
release, and rewording it is not a breaking change. A handler that branches on
message, or on a substring of it, breaks silently on a release that changes
one word — and it breaks in the direction where a rejection reads as an
unrecognised one. Key every branch off code.
Print message. Match code.
2. data present and null is a SUCCESS.
A read can succeed with no content. That answers 200 with {"data": null}.
There is no error key on it, because there is no error.
Test for the PRESENCE of error, not for a null data. A client that reads
"data is null, so this failed" reports an empty result as a failure. A client
that reads error === null on a success gets undefined, because the key is
absent — and undefined is falsy, so that test works by accident and stops
working the moment it is inverted.
details — the bound that was broken
details appears only on the rejections that carry a numeric bound. Its shape
is:
| Key | Meaning |
|---|---|
field | The request field the bound applies to |
limit | The bound. The value you must respect |
actual | The value that broke it. What the request asked for |
Worked example — MARGIN_INSUFFICIENT on an order that needs more collateral
than the account has free:
{ "field": "margin", "limit": "2.0", "actual": "15.0" }
limit is the free collateral (2.0) and actual is the requirement
(15.0). Read it as "the bound, and the value that broke the bound" — not as
"minimum, maximum". On ORDER_INVALID_PRICE the same pair reads as the tick
size and the price sent.
Three rejections carry details today: ORDER_INVALID_PRICE,
ORDER_INVALID_SIZE and MARGIN_INSUFFICIENT. A retired /info read also
carries a details naming its replacement — see
UNKNOWN_TYPE below.
Do not require details. Read it when it is there and fall back to code
plus message when it is not.
The code catalog
Codes are namespaced by prefix. One code always answers with one status, with
the single documented exception on UNKNOWN_TYPE.
The HTTP column is the status the code carries when ADMISSION mints it. On
/exchange most ORDER_*, MARGIN_*, MARKET_* and ASSET_* rules run at
commit instead, and a commit verdict rides back on a 200 — see
commit-time codes for which codes those
are and where to read them.
ORDER_* — the order body
code | HTTP | details | Cause and caller action |
|---|---|---|---|
ORDER_NOT_FOUND | 404 | — | The oid, cloid or TWAP names no live order. It filled, it was cancelled, or it never existed. A cancel that gets this is harmless — the order is already gone. Do not retry |
ORDER_ZERO_SIZE | 400 | — | Size is zero or negative. Send a positive size |
ORDER_INVALID_PRICE | 400 | ✅ | The price is off the tick grid. Round to a multiple of details.limit and resend |
ORDER_INVALID_SIZE | 400 | ✅ | The size is off the lot grid. Round to a multiple of details.limit and resend |
ORDER_BELOW_MIN_NOTIONAL | 400 | — | Price × size is under the market minimum. Increase the size |
ORDER_SELF_TRADE | 400 | — | The two sides of an rfq_accept are one party. RFQ lane only — on the order book, self-trade prevention CANCELS an order and never mints this code. Quote or accept from an account outside the taker's STP group |
ORDER_DUPLICATE_CLOID | 400 | — | The cloid is already in use on this account, or two legs of one action share it. Use a fresh one. Do not treat this as a failure to place — check whether the first submission rested. An attempt the COMMIT refused gives its cloid back, so a re-signed retry may reuse that handle (not live yet) |
MARGIN_* — collateral
code | HTTP | details | Cause and caller action |
|---|---|---|---|
MARGIN_INSUFFICIENT | 400 | ✅ | The account cannot fund the requirement. details.limit is what is free, details.actual is what is needed. Reduce the size, cut the leverage, or add collateral |
AUTH_* — signature and authorization
All three answer 401. None carries details.
code | Cause and caller action |
|---|---|
AUTH_UNAUTHORIZED | The signer is not allowed to act for this account. Check the owner you sent and the key you signed with |
AUTH_BAD_SIGNATURE | The signature does not recover. The bytes are malformed, the recovery byte is wrong, or the signing-domain chainId is wrong — a wrong chainId recovers a valid but different address. Match the network chainId and re-sign |
AUTH_AGENT_FORBIDDEN | The signer is an agent of the account, but this action is not one an agent may take, or the approval has expired. Sign with the owner key, or re-approve the agent |
An AUTH_* failure is never fixed by a retry. The same bytes recover the
same address. Correct the signing input first.
MARKET_* — the market
code | HTTP | details | Cause and caller action |
|---|---|---|---|
MARKET_NOT_FOUND | 404 | — | The coin symbol or asset index names no market. Read the market list and use a symbol from it |
MARKET_INACTIVE | 400 | — | The market exists but does not accept this order: trading is disabled, the pair is closed, or the market is reduce-only. Only a closing order is admitted while a market is reduce-only. A perp that a delist halted or settled, or that governance paused, answers PRECONDITION_FAILED instead |
MARKET_OI_CAP | 400 | — | Open interest is at the market cap. Nothing about your request is wrong. Wait, or trade another market |
ASSET_* — spot balance
code | HTTP | details | Cause and caller action |
|---|---|---|---|
ASSET_INSUFFICIENT_BALANCE | 400 | — | The spot balance cannot fund the transfer, withdrawal or spot order. Check the free balance — a held balance is not spendable. Not live yet for a spot order: a live node accepts an unfunded spot order as a no-op |
RATE_LIMITED
code | HTTP | details | Cause and caller action |
|---|---|---|---|
RATE_LIMITED | 429 | — | The request budget is spent. No retry hint is sent — there is no retry_after_ms field and no Retry-After header. Compute the wait from the published refill rate |
The bucket refills at 20 weight per second. An /info read costs 1 weight, so
it is affordable again after 50 ms; an /exchange write costs 5 weight, so
250 ms. See rate limits.
NONCE_REPLAYED
code | HTTP | details | Cause and caller action |
|---|---|---|---|
NONCE_REPLAYED | 200 | — | The block builder dropped the action: this account already used the nonce, or it sits more than 64 below the newest one. Nothing committed and the nonce is not consumed. Do not retry at the same nonce — re-sign at a higher one. Not live yet: a live node drops the replay in silence, and the caller waits out the order window |
The 200 is not a mistake. This is a commit verdict, not an admission
refusal, and every commit verdict rides a 200. On an order action the same
object arrives as statuses[0].error. See
a replayed nonce for the 64-wide window and
how a wrong clock walks an account out of it.
Request-shape codes
code | HTTP | details | Cause and caller action |
|---|---|---|---|
INVALID_REQUEST | 400 | sometimes | A field is missing, unparseable, or out of range. message names the field. Fix the body — a retry of the same bytes gets the same answer |
UNKNOWN_TYPE | 400 / 410 | on 410 | The /info type names no read. See below |
NOT_FOUND | 404 | — | A named resource does not exist — a vault, a sub-account. Check the identifier. An unknown ACCOUNT is not this: an address never seen on-chain answers 200 with a zeroed record |
ACTION_UNSUPPORTED | 400 | — | The action decodes, but this build has no path for it. It is a system-only action, or a capability that is not open yet. Do not retry — see the action catalog |
PRECONDITION_FAILED | 400 | — | A state rule refused the action, and the rule has no code of its own. message carries the reason. This is the catch-all: read message to learn what happened, then fix the state or the request. Do not match on that message — if you need the rule as a branch, ask for a code for it |
UNKNOWN_TYPE and the one 410
UNKNOWN_TYPE answers two statuses, and they mean different things:
-
400— thetypenames no read on this API. It is misspelled, or the read was removed and its answer is gone. Fix the request. -
410— thetypenamed a public read whose answer moved to another read. The error carriesdetails.use, naming the read to call instead:{"error": {"code": "UNKNOWN_TYPE","message": "gov_state is retired; use validator_votes (time-ranged, served from the archive)","details": { "field": "type", "use": "validator_votes" }}}A client can follow the move from
details.usealone. The full list of moved reads is in removed reads.
410 is used because neither alternative is true: 400 claims the request is
malformed, and it is well formed; 404 claims the read never existed, and it
did.
Server-side codes
code | HTTP | details | Cause and caller action |
|---|---|---|---|
INTERNAL | 500 | — | Our defect, not your request. Arithmetic overflow or a broken invariant. The message is always the literal string internal error — the internal sentence never reaches you. Retry, then report it. There is nothing to fix in the request |
UNAVAILABLE | 503 | — | An upstream the request needs is down or not configured. The gateway mints this; a node never does. Back off from 200 ms and retry. A sustained UNAVAILABLE is an operator incident, not a client bug |
Neither of these is caused by your request, so neither is fixed by changing it. Retry with backoff. Do not burn a new nonce per attempt on a write — see idempotency.
Statuses that carry no envelope
| HTTP | When |
|---|---|
405 | The method is wrong. Every endpoint here is POST. The router refuses the request before an envelope exists, so there is no error object to read |
Commit-time rejections
Some order failures happen after the HTTP reply, because they are only knowable in block-execution context — a self-trade at match time, a reduce-only leg that closed between admission and dispatch, a margin check that fails once other fills landed first.
For an order-type action, the per-leg entry in statuses carries the same
error object as the envelope, with the same code. See
per-order statuses.
The order_updates WS channel also
pushes a {"status":"rejected","reason":"<free text>"} event. reason there is
free text, not a code — treat it as human-readable only. order_updates
carries no action_hash, so correlate by cloid, per
error handling.
Only ORDER-type actions get that channel. For every other action — a
twap_order, a cancel, a margin, vault or staking write — a commit-time
rejection reaches you in the HTTP response or nowhere. The /exchange call
waits for the commit, so read the verdict there. If you got a 202, the wait
expired: re-read the state the action was meant to change. The full rule and
a per-class table are in
accepted is not committed.
How to write the handler
- Branch on the presence of
error. Present means failure. Absent means success,data: nullincluded. - Switch on
error.code. Never onerror.message. - Group by prefix for the default arm. An unrecognised
ORDER_*is an order problem; an unrecognisedAUTH_*is a signing problem. A new code added in a later release then lands in the right arm instead of the unknown one. - Read
detailswhen it is there.details.limitis the value to round to, or the balance to respect. - Retry only
RATE_LIMITED,INTERNALandUNAVAILABLE. Every other code returns the same answer to the same bytes.
See also
POST /exchange— write pathPOST /info— read path- Rate limits
- Idempotency — how to retry a write safely
- Error handling guide — patterns for a production client