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, includingmaxStakewhen a stale-price cap is what refused you.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 Predicta 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-Predicta-User header. |
invalid_cursor | Every cursored read | Malformed, or issued under a different sort/dir. Restart from the first page rather than returning a confidently wrong slice. |
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. |
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 Predicta-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 — 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. |
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. Reads never create one, so check the id before retrying. |
unknown_deposit | No such deposit for this operator. |
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 has already been spent. | Do not retry. If this was a retry, resubmit with the same clientOrderId to get the original order back. |
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. |
Three of these four mean re-quote. The fourth means wait.
price_stale, quote_expired and quote_already_used say the request was correct and the world changed: ask for a fresh quote and show the player the new price. reconciliation_required says nothing about the price and everything about an unresolved order — re-quoting there risks a second position against the same reservation.
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. The response carries maxStake. |
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_collateral | The execution layer has no collateral available for this order. |
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. |
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. |
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.
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 — Predicta 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. |
409 idempotency_conflict | Not returned. There is no Idempotency-Key header on any endpoint; orders are idempotent on clientOrderId instead, and a replay is a 200 rather than a conflict. |
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 — ReconciliationRequiredError carries holdAuthorization: true, RateLimitError carries retryAfterSeconds, InsufficientScopeError carries required — so the classification above is already made for you. 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.

