Reference
Errors
Every failure is a JSON body with a machine-readable error code. Branch on the code, not on the message: messages are for humans and may be reworded.
The error shape
{ "error": "quote_expired" }
{ "error": "invalid_request", "detail": [ { "path": ["side"], "message": "..." } ] }
{ "error": "insufficient_scope", "required": "orders:write", "message": "..." }erroris always present and is the field to branch on.detailis present on validation and stake failures and carries the specific reason. On a quote refusal it is the only field besideerror: a stake cap arrives inside that string as prose, not as a named field. There is nomaxStakeon the wire. Parse the code, show the detail, and re-quote at a smaller size rather than trying to read a number out of it.orderIdis present on409 quote_already_usedand on nothing else. It names the order that spent the quote, and it is the only recovery path that code has.priceMoveis present on409 price_movedand on nothing else. It carriesfromCents,toCentsandadverseCents, so you can show the player what the price did without re-quoting to find out.requiredis present oninsufficient_scopeand names the scope the route wanted, so the fix never has to be guessed.messageis prose for a log line. It is not stable and must not be parsed.
The one code that is not a failure
409 reconciliation_required means HOLD the authorization
The venue took the order or it did not, and Parity does not yet know which. This is not a rejection. Collateral stays committed and the operator must not release the authorization.
GET /api/v1/orders/{id} reports it honestly: status: "submitted", settlementState: "pending", actualDebit of "0.00" and the whole authorization still showing as releaseAmount. Poll until settlementState becomes settled.
Rendering an unresolved order as failed is the most expensive mistake available on this API: releasing the authorization on an order that did in fact execute leaves a real position with no cash behind it, and nothing later tells you it happened.
Every error this API returns
400: the request needs fixing
| error | Where | Meaning |
|---|---|---|
invalid_json | Quotes, orders | The body did not parse as JSON. |
invalid_request | Quotes, orders, order lookup | Schema validation failed, or a clientOrderId arrived without an externalUserId. |
missing_user | Quotes, orders, deposit address | No X-Parity-User header. |
invalid_cursor | Events, settlements, reconciliation feed | Malformed, or issued under a different sort/dir. Restart from the first page rather than returning a confidently wrong slice. The order, position and sandbox ledger feeds do NOT refuse: see “A malformed cursor is not always refused” below. |
invalid_limit | Every paged read | Outside the route’s range. |
invalid_category | Events | Not a canonical category. Refused rather than ignored: a bad filter that returns the whole catalogue is a bug that looks like working software. |
invalid_sort | Events | Not one of trending, volume, closing-soon, new. |
invalid_dir | Events | Not asc or desc. |
invalid_date | Events | closesAfter / closesBefore were not ISO-8601. |
invalid_tradable | Events | tradable was not true or false. |
invalid_type | Reconciliation feed | Unknown item type in a ?type= filter. |
invalid_from | Reconciliation | The window bound was not ISO-8601. |
invalid_since | Deprecated settlement feed | Not ISO-8601. Refused rather than treated as “from the beginning”. |
invalid_outcome | Settlements | outcome was not won, lost or void. |
invalid_settled_from | Settlements | settledFrom was not ISO-8601. |
invalid_settled_to | Settlements | settledTo was not ISO-8601. |
invalid_jurisdiction | Jurisdiction assertion | country was not a two-letter ISO 3166-1 alpha-2 code. |
invalid_origin | Embed sessions | returnOrigin could never match a browser Origin header. |
invalid_region | Embed sessions | A region we do not know. |
invalid_body | Sandbox controls | The body was not a JSON object. Distinct from invalid_json, which is a parse failure. |
scenario_invalid | Sandbox scenarios | The scenario is not a partial fill, or its size is unusable. |
position_not_open | Sandbox resolutions | The request does not name a position that can still be resolved. |
no_contracts | Price stream | No usable contract ids in ?contracts=. |
401 / 403: credentials, scope and eligibility
| Status | error | Meaning |
|---|---|---|
401 | missing_credentials | No Authorization header, or not a Bearer token. |
401 | invalid_key | Unrecognised key. |
401 | revoked_key | The key was revoked. |
403 | insufficient_scope | The key does not carry the scope this route needs. required names it. Checked before the rate limit, so a scope mistake never arrives disguised as a 429. |
403 | operator_suspended | The operator is suspended. |
403 | sandbox_only | A balance, ledger or funding endpoint called by a live tenant. Those model a Parity-held balance and exist for sandbox integration only. |
403 | market_not_offered | The market exists; this operator is not offered it. Deliberately not a 404, because the market is real and saying otherwise would send an integrator to debug their ids. |
403 | quote_not_yours | The quote belongs to another operator. A quote is an offer to one party. |
403 | over_wager_limit | The stake is above a maximum wager in force for this account: either the number the operator set for every account under it, or a tighter one set against this account. detail names the ceiling in prose; there is no maxStake field. Send a smaller stake and it works immediately. |
403 | over_period_limit | This account has staked its allowance for a rolling window. Not the same answer as over_wager_limit: the stake is not too large, and a smaller one may not help. The allowance refills with time, so wait rather than re-price. detail names what has been staked, over what window, and against which ceiling. |
403 | over_order_limit | The order is above the operator’s own tenant-wide ceiling on one order’s notional. Kept separate from the two above because it is a different control on a different screen, and covers sells as well as buys. |
403 | user_suspended | You suspended this player. Reported as a 403 for the same reason as your offering policy: the payload is correct, and the refusal is your own configuration speaking. |
404: not found
| error | Meaning |
|---|---|
market_not_found | No such contract, on a quote request. |
quote_not_found | No such quote, on an order request. |
unknown_event | No such event id or slug in this operator’s catalogue. |
unknown_order | No such order for this operator. Covers “not yours” as well, deliberately: distinguishing them would make the route an oracle for whether a guessed id is real. |
unknown_position | Same rule, for a position. |
unknown_user | No such player for this operator, on the settlement register, the reconciliation feed and the two sandbox balance reads. The order and position lists do not raise it: an id that never traded returns an empty page there, so check the id rather than reading the emptiness as a fact. |
unknown_deposit | No such deposit for this operator. |
position_not_found | Sandbox resolutions. No such position for this operator. |
scenario_not_found | Sandbox scenarios. No armed scenario with that id. |
409: the state moved
| error | Meaning | What to do |
|---|---|---|
price_stale | The upstream price is too far behind to trade on. Returned by both the quote and the order path. | Wait for the next tick and re-quote. |
quote_expired | The quote passed expiresAt. | Re-quote and re-confirm the new price. |
quote_already_used | That quote was spent by an order that is not the one you are sending. It cannot be the same clientOrderId: a resend of that id replays as a 200 and never reaches this branch. | Read orderId on the body. It names the order that spent the quote, and GET /api/v1/orders/{id} says what it bought. Then quote afresh for whatever intent is still unfilled. Your own clientOrderId looks up an empty page here, so do not reach for it. |
reconciliation_required | The venue did not answer in time. The order may or may not exist there. | HOLD the authorization. Poll GET /api/v1/orders/{id} until settlementState is settled. Never release. |
not_cancellable | This order's transmissionState is not not_transmitted, so it cannot be withdrawn. The body carries the state. | unknown: keep holding the authorization and wait for reconciliation. resolved: the money is already decided, so apply the order's own operatorMoney. |
resolution_conflict | A sandbox position was already settled to a different resolution. | Sandbox only. The first resolution stands and is not rewritten; ask for the same one to replay it. |
price_moved | The venue's live book no longer offers the price the quote named. Adverse moves only: a move in the player's favour fills at the better price. priceMove on the body carries fromCents, toCents and adverseCents. | Show the new number and let the player decide. You do not have to re-quote blind to find it. |
price_unverified | We could not reach the venue to confirm the current price, so we did not accept the bet. Not the same as price_stale, which is a verdict on the age of a price we hold. | Retry in a moment. Nothing was accepted, so it is safe. |
market_paused | A five-minute contract with no executable book this instant. | Re-quote in a moment. It clears by itself. |
line_not_available | An outcome on this contract is executable above the wagerable ceiling, so the line is not wagerable. It is not a suspension: the verdict is recomputed from the book on every request. detail names the offending price. Buys only, so a holder of the near-certain side can always exit. | Re-quote rather than retrying a spent quote. It clears when the book moves back inside the band. |
idempotency_conflict | A clientOrderId that already names a different order. Two orders want one key and the first one has it. | Do not retry. Reconcile your own reservation against the order that holds the key. |
order_cancelled | On confirm-reserve: the order was cancelled before you confirmed. | Release the authorization. Nothing stands behind it. |
not_an_intent | On confirm-reserve: the order is not awaiting a reserve. It is already at a venue, filled or rejected. | Do not absorb this. Read the order back and act on its own settlementState and operatorMoney. |
Some of these mean re-quote. The rest mean wait, or stop.
price_stale, price_moved, price_unverified, quote_expired, market_paused and line_not_available say the request was correct and the world changed: ask for a fresh quote and show the player the new price.
reconciliation_required and not_cancellable say nothing about the price and everything about an order whose money is not yours to move yet. Re-quoting there risks a second position against the same reservation, and releasing there strands a live position with no cash behind it.
quote_already_used and idempotency_conflict mean something else already happened under an identifier you thought was free. Neither is retryable: go and read the order that holds it.
422: accepted shape, refused business rule
| error | Meaning |
|---|---|
market_not_open | The contract has closed, resolved or been voided. detail carries the status. |
no_price | No usable price for that side. Show “no price”. |
invalid_stake | The stake buys zero contracts. |
stake_exceeds_stale_limit | The price is stale enough to cap the stake. detail names the cap in prose; there is no maxStake field on the wire. |
market_unavailable | The contract closed between quote and order. |
no_eligible_venue | No source is configured to execute this contract. A configuration problem: it does not resolve itself on the next tick. |
insufficient_position | The sell is larger than the position held. Size the exit in contracts from the position you read back. |
insufficient_funds | THE CUSTOMER cannot back this order, and it is not the same refusal as insufficient_collateral below it. Nothing was sent to the venue and no collateral was committed. detail names what they hold and what the order needed. Raised only where Parity holds the wallet; if you hold your customers’ balances yourself, you will never see it and the check is yours to make. For those tenants it IS a reserve: a placed order holds its stake until it fills, is rejected or is cancelled, so a customer with $120 who has $60 working may place $60 more and no more, whether the orders arrive together or hours apart. A rejection, a cancellation and the unfilled remainder of a partial fill all return it. If you hold your customers’ balances yourself, Parity keeps nothing to be short of and the check is yours: debit or reserve on your side, and do not treat the absence of this error as a reservation. |
insufficient_collateral | YOUR float at the venue is short, which is a different problem with a different fix: top it up. Do not collapse this and insufficient_funds into one branch, because one is your money and the other is your customer’s. |
rejected | The execution layer refused. detail carries the reason. |
route_not_supported | Funding only. That asset and chain pair is not in the live catalogue. |
simulation_not_permitted | You sent executionIntent: "simulate" and your tenant’s execution mode does not allow it. The credential is fine and the request is understood: send executionIntent: "record_intent" instead, or have the ceiling raised. |
429, 5xx
| Status | error | Meaning |
|---|---|---|
429 | rate_limited | Over the per-key allowance. The body carries retryAfterSeconds and the response adds Retry-After. |
500 | internal_error | An unhandled failure. Retry with backoff; report it if it persists. |
502 | bridge_unavailable | Funding only. The bridge could not be reached; nothing was recorded, so retry is safe. |
503 | funding_unavailable | Funding only. No treasury destination configured. Not retryable without an operations change. |
503 | no_address_for_chain | Funding only. No deposit address could be issued on that chain. |
503 | embed_unavailable | Embed sessions are not configured on this deployment. Ours to fix, not yours. Not retryable without an operations change on our side. |
503 | stream_capacity | The price stream is at its live-connection limit for this instance. Retry-After says when to come back. |
A failed bridge poll is not an error
Reading a deposit while the bridge is unreachable returns 200 with the stored lifecycle and a syncError field, not a 502. The state may be stale; the money is not gone, and a status screen should say so.
A malformed cursor is refused on every feed
Every paged feed answers a cursor it cannot decode with 400 invalid_cursor: /api/v1/events, /api/v1/orders, /api/v1/positions, /api/v1/settlements, /api/v1/reconciliation/feed and the sandbox GET /api/v1/users/{id}/ledger. One rule, one code, one status.
Three of those six used to restart at page one instead. What it cost: a consumer that stored a truncated cursor re-read its whole history and reported a successful page while doing it. They refuse now, so a bad cursor is something you see.
A cursor naming a row that has gone is not the same thing
On /api/v1/orders, /api/v1/positions and the ledger read, a cursor that decodes cleanly but names a row that is no longer there still yields the first page. That is deliberate: the alternative is an empty page, which is indistinguishable from the end of the feed and is the worst available answer.
So key every credit on the record's own id rather than on having advanced, and treat an unexpectedly full first page after a resume as a signal rather than as a busy night.
Rate-limit headers are not on every response
The gate produces RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset on every request it authorises, and most routes pass them back. Two that do not are POST /api/v1/quotes and POST /api/v1/orders, which answer without them on success and on failure alike. Several error paths elsewhere omit them too: the 404 for an unknown player, every 403 sandbox_only, and the 400 and 500 branches of the order reads.
So do not build a budget tracker that assumes a header on every reply. Read them where they arrive, keep your own count between reads, and treat a 429 as the authoritative signal. The 429 body always carries retryAfterSeconds and the response always carries Retry-After.
Codes this API does not return
Named here because they appear in most payments-shaped API specifications, and their absence is a fact worth knowing rather than a gap to discover in production.
| Code | Status |
|---|---|
422 insufficient_balance | Not returned, and never will be. You own your customers' cash ledger. Parity holds no player funds and has no balance of theirs to check against. Reserve on your own wallet before quoting; a stake is refused here for price, freshness and eligibility only. |
Idempotency-Key | No endpoint accepts the header. Orders are idempotent on clientOrderId instead, and a replay of the same id is a 200 rather than a conflict. A clientOrderId reused for a DIFFERENT order is 409 idempotency_conflict, which is listed with the other 409s above. |
503 venue_unavailable | Not returned. An unreachable source surfaces as 409 price_stale, 409 reconciliation_required or 422 no_eligible_venue. /api/health returns 503 only when the database is down. |
A retry policy that works
// Branch on the class of failure, not on the status code alone.
function classify(status: number, error: string) {
if (error === 'reconciliation_required') return 'hold'; // do NOT release, do NOT re-quote
if (status === 409) return 'requote'; // price moved or quote spent
if (status === 429 || status >= 500) return 'backoff';
if (status === 401 || status === 403) return 'credentials';
return 'fix'; // 400 and 422: retrying changes nothing
}- hold: keep the authorization, poll
GET /api/v1/orders/{id}and act only whensettlementStateissettled. If you never received anorderId, find it withGET /api/v1/orders?externalUserId=&clientOrderId=. - requote: start a new quote. Show the player the new price before submitting.
- backoff: exponential, with jitter. For an order, resubmit with the same
clientOrderId; that is what makes the retry free. - credentials: read
requiredon aninsufficient_scopeand issue a key that carries it. Waiting fixes nothing. - fix: never retry unchanged. A
422retried in a loop is a loop.
The SDK raises these as typed errors, so the classification above is already made for you. ReconciliationRequiredError carries holdAuthorization: true, RateLimitError carries retryAfterSeconds, InsufficientScopeError carries required, and QuoteAlreadyUsedError carries the colliding orderId. See the SDK.
Rate limits
The limiter runs on every /api/v1 route and every response carries the current window on three headers, whether it succeeded or not. It is documented in full on Rate limits. Read it before you write your polling loop: a second ceiling binds every key you hold together, and the headers on this response do not describe it.

