Skip to main content

Errors

info

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" }
}
}
FieldPresenceMeaning
codealwaysThe stable machine-readable identifier. Match on this.
messagealwaysOne human sentence. Never match on it.
detailsoptionalThe bound the request broke. Omitted when the rejection names no bound — never sent as {}

The two rules callers get wrong

danger

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.

danger

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:

KeyMeaning
fieldThe request field the bound applies to
limitThe bound. The value you must respect
actualThe 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

codeHTTPdetailsCause and caller action
ORDER_NOT_FOUND404The 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_SIZE400Size is zero or negative. Send a positive size
ORDER_INVALID_PRICE400The price is off the tick grid. Round to a multiple of details.limit and resend
ORDER_INVALID_SIZE400The size is off the lot grid. Round to a multiple of details.limit and resend
ORDER_BELOW_MIN_NOTIONAL400Price × size is under the market minimum. Increase the size
ORDER_SELF_TRADE400The 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_CLOID400The 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

codeHTTPdetailsCause and caller action
MARGIN_INSUFFICIENT400The 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.

codeCause and caller action
AUTH_UNAUTHORIZEDThe signer is not allowed to act for this account. Check the owner you sent and the key you signed with
AUTH_BAD_SIGNATUREThe 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_FORBIDDENThe 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
tip

An AUTH_* failure is never fixed by a retry. The same bytes recover the same address. Correct the signing input first.

MARKET_* — the market

codeHTTPdetailsCause and caller action
MARKET_NOT_FOUND404The coin symbol or asset index names no market. Read the market list and use a symbol from it
MARKET_INACTIVE400The 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_CAP400Open interest is at the market cap. Nothing about your request is wrong. Wait, or trade another market

ASSET_* — spot balance

codeHTTPdetailsCause and caller action
ASSET_INSUFFICIENT_BALANCE400The 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

codeHTTPdetailsCause and caller action
RATE_LIMITED429The 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

codeHTTPdetailsCause and caller action
NONCE_REPLAYED200The 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

codeHTTPdetailsCause and caller action
INVALID_REQUEST400sometimesA 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_TYPE400 / 410on 410The /info type names no read. See below
NOT_FOUND404A 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_UNSUPPORTED400The 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_FAILED400A 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 — the type names no read on this API. It is misspelled, or the read was removed and its answer is gone. Fix the request.

  • 410 — the type named a public read whose answer moved to another read. The error carries details.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.use alone. 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

codeHTTPdetailsCause and caller action
INTERNAL500Our 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
UNAVAILABLE503An 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
info

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

HTTPWhen
405The 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.

danger

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

  1. Branch on the presence of error. Present means failure. Absent means success, data: null included.
  2. Switch on error.code. Never on error.message.
  3. Group by prefix for the default arm. An unrecognised ORDER_* is an order problem; an unrecognised AUTH_* is a signing problem. A new code added in a later release then lands in the right arm instead of the unknown one.
  4. Read details when it is there. details.limit is the value to round to, or the balance to respect.
  5. Retry only RATE_LIMITED, INTERNAL and UNAVAILABLE. Every other code returns the same answer to the same bytes.

See also