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": "..." }
{ "error": "over_wager_limit", "detail": "...", "maxStake": 50 }erroris always present and is the field to branch on.detailis present on validation, quote and order refusals and carries the specific reason as prose for a person. Do not parse numbers out of it: the figures a client can act on are named fields, listed below.maxStake(a number, major units) is present on403 over_wager_limitand422 stake_exceeds_stale_limit: the largest stake that would be accepted. Re-quote at or under it.maxCentsis present on409 line_not_available: the executable price, in cents, that put the line outside the wagerable band. It is a price, not an amount of money, which is why it is not calledmaxStake.retryAfterMsis present on409 over_tenant_exposure: how long until the window that is full rolls over.requestedStakeandsupportedStakeare present on409 stake_exceeds_available_depth: what you asked for, and the largest stake the book can fill at these terms.sellableContractsandminimumContractsare present on422 insufficient_positionand422 below_venue_minimumwhen a live cash out is refused at quote time: the most contracts the player could sell now, and the smallest sale the market accepts.orderIdis present on409 quote_already_used, where it names the order that spent the quote, and on409 idempotency_conflict, where it names the order that already holds yourclientOrderId.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. OnPOST /api/v1/embed-sessionsit is an array (that route needs three scopes) and amissingarray beside it names the ones your key lacks.retryAfterSecondsis present on every429, and so is theRetry-Afterheader. They carry the same number of seconds.messageis prose for a log line. It is not stable and must not be parsed.
The request id
Most partner responses, success and failure alike, carry a parity-request-id header: a fresh random value Parity attaches as the response goes out. The 429s from the image and embed-session limits do not.
Quotes and orders are different, because those are the calls a dispute is about. On POST /api/v1/quotes and POST /api/v1/orders you may send your own id in an x-request-id header: 1 to 128 characters of letters, digits, ., _, : and -. Parity uses it when it is well formed and mints a random one when it is absent or is not, and either way the response carries it in both x-request-id and parity-request-id, success and refusal alike. That id is stored with the quote, the order and any refusal, so Parity support can find the call by it. It is yours to choose, so it is not unique: keep it unique per call if you want it to identify one.
When you report a problem, send the request id together with what else identifies the call: the route, the time (UTC), the player's externalUserId, and the quoteId, orderId or your clientOrderId where there is one, with the error code you received.
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 is still confirming 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 | Every route with a JSON body: quotes, orders, cancel, confirm-reserve, jurisdiction and limits writes, embed sessions, deposit address, sandbox controls | The body did not parse as JSON. |
invalid_request | The same routes, and the order list | Schema validation failed (detail lists the issues), or on the order list a clientOrderId arrived without an externalUserId. |
missing_user | Quotes, orders, jurisdiction and limits, deposit address | No X-Parity-User header. |
invalid_cursor | Events, orders, positions, settlements, reconciliation feed, sandbox ledger | Malformed, or issued under a different sort/dir. Restart from the first page rather than trusting a slice. See “A malformed cursor is refused on every feed” below. |
invalid_limit | Events (1 to 100), settlements, reconciliation feed | Outside the route’s range. The order, position and ledger lists do not refuse a limit: they bring it into range. |
invalid_sport | Events | A sport that is not in your catalogue. The message lists the sports available to you. |
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 resolutions | A missing or invalid field such as positionId or resolution. A body that does not parse as JSON is invalid_json, as everywhere else. |
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=. |
Swipe the table sideways for more columns.
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 account is not active. Returned for any status other than active, not only a suspension. |
403 | operator_not_permitted | Aggregators: the x-parity-operator header names an operator this key is neither for nor the direct parent of. The refusal does not say whether that operator exists. |
403 | not_a_sandbox_operator | A sandbox control called by a tenant that is not a sandbox. In practice sandbox_only answers first. |
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. maxStake on the body is that ceiling. Send a stake at or under it 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. Returned on quotes and orders. Reported as a 403 for the same reason as your offering policy: the payload is correct, and the refusal is your own configuration speaking. |
403 | sell_not_available | A sell (cash out) on a live account while cashing out before settlement is turned off. Returned on quotes and orders. The position stays open and settles normally. |
Swipe the table sideways for more columns.
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. |
Swipe the table sideways for more columns.
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 has moved off 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 | Quotes. A five-minute contract with no executable book this instant. Also a contract that settles on a protocol Parity does not trade, with detail “This market is not available to trade yet.”; such contracts are withdrawn from the catalogue when ingest learns their protocol. | Re-quote in a moment; the five-minute case clears by itself. The protocol case does not: stop offering the contract. |
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. orderId on the body names the order that holds the key; reconcile your own reservation against it. |
over_tenant_exposure | Your tenant has taken as much stake as its per-window exposure budget allows. Quotes and orders. The request is correct; the window is full. | Wait retryAfterMs (on the body) and try again. Not a 429: it is about stake, not request count. |
stake_exceeds_available_depth | Quotes only. The book cannot fill the whole stake at these terms. requestedStake and supportedStake are on the body. Refused rather than quietly quoted smaller, so a customer is never charged for a bet they did not ask for. | Offer the customer supportedStake, and re-quote at that stake if they accept. |
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. |
Swipe the table sideways for more columns.
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.
trading_paused, order_lane_unavailable and live_execution_unavailable are 503s: nothing was quoted or placed, and the right response is to wait and try again, not to re-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, or a live cash out was sized with stake instead of contracts. |
stake_exceeds_stale_limit | The price is stale enough to cap the stake, or no book can be read for the contract. maxStake on the body is the cap. Re-quote at or under it. |
market_unavailable | Orders. The contract closed between quote and order, or it settles on a protocol Parity does not trade (detail: “This market is not available to trade yet.”). |
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. On the order path, for every account. On a live account it is also refused when the sell is quoted, counting contracts already promised to this player’s other cash outs in progress, and the body carries sellableContracts and minimumContracts. Size the exit in contracts from the position you read back. |
below_venue_minimum | Quotes, live accounts. The cash out is smaller than the market’s minimum sale (the market’s own minimum where it publishes one, otherwise 5 contracts). A whole position smaller than the minimum cannot be cashed out at all: it stays open and settles normally. The body carries sellableContracts and minimumContracts. |
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. |
Swipe the table sideways for more columns.
429, 5xx
| Status | error | Meaning |
|---|---|---|
429 | rate_limited | Over the per-key allowance, or over the ceiling shared by every key your operator holds (the message says which). Retry-After is always set, and retryAfterSeconds carries the same figure on the body. |
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 | trading_paused | Quotes and orders. The trading pause on your account is on: no new bets are taken, open positions are unaffected and settle normally. An order still waiting to be sent is not sent while the pause is on; where Parity sends your orders under your live operator policy it is withdrawn (status cancelled) and its hold released. An order already at a venue is not touched. Retry once the pause is lifted. |
503 | order_lane_unavailable | Quotes, live accounts. The service that sends live orders has not reported recently, for example during a restart. Nothing was quoted. Retry in a minute. |
503 | live_execution_unavailable | Quotes and orders. Live execution is unavailable for this account right now. Nothing was placed and no money moved. Retry later. |
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 temporarily unavailable on our side. Nothing to fix in your request; retry later. |
503 | stream_capacity | The price stream is at its live-connection limit for this instance. Retry-After says when to come back. |
Swipe the table sideways for more columns.
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.
Embed-session and price-stream codes
A request made with an embed session (Authorization: Embed <token>) on the embed surface, or on GET /api/v1/stream/prices when a session is presented, is refused with one of these. The embed-session mint itself adds two 422s.
| Status | error | Meaning |
|---|---|---|
401 | missing_session | No session. A Bearer API key sent here gets this too, with a message saying so: an API key is not a credential for the browser surface. |
401 | token_in_query | The token was sent as ?token=. Send it in the header. |
401 | malformed_token | Not the shape of a session token. |
401 | invalid_token | The token does not verify. |
401 | unknown_session | No such session. |
401 | session_expired | The session is past expiresAt. Mint a new one. |
401 | session_revoked | The session was ended. |
403 | origin_mismatch | The browser Origin is not the returnOrigin the session was minted for. |
403 | operator_suspended | The operator is not active. |
403 | user_suspended | You suspended this customer. |
429 | rate_limited | Over the per-session limit or the operator ceiling. Retry-After is set and retryAfterSeconds is on the body. |
503 | signing_unavailable | Sessions are temporarily unavailable on our side; retry later. |
422 | invalid_external_user_id | Mint only: a session bound to an operator-backed book needs an externalUserId of 1 to 128 characters from A to Z, a to z, 0 to 9 and . _ : -. |
422 | invalid_operator_book | Mint only: the operatorBookId is not an enabled book of yours. |
Swipe the table sideways for more columns.
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.
Refusing is what makes a bad cursor visible: a consumer that stored a truncated cursor sees a 400 rather than silently re-reading its whole history as a successful page.
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 has since been removed from the result 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. These answer without them:
- Every
400and500from/api/v1/events. - Every response from the funding routes and the sandbox controls.
- Every
403 sandbox_only,404 unknown_userand404 unknown_deposit.
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. Every 429 carries Retry-After and the same figure as retryAfterSeconds on the body.
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 | Idempotency is keyed on clientOrderId rather than on a header, 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 price_unverified, 409 reconciliation_required or 422 no_eligible_venue. /api/health returns 503 only when the database is down. |
Swipe the table sideways for more columns.
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 authenticated /api/v1 route, and most responses carry the key's current window on three headers (the exceptions are listed above). 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.

