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

json
{ "error": "quote_expired" }

{ "error": "invalid_request", "detail": [ { "path": ["side"], "message": "..." } ] }

{ "error": "insufficient_scope", "required": "orders:write", "message": "..." }
  • error is always present and is the field to branch on.
  • detail is present on validation and stake failures and carries the specific reason, including maxStake when a stale-price cap is what refused you.
  • required is present on insufficient_scope and names the scope the route wanted, so the fix never has to be guessed.
  • message is 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

errorWhereMeaning
invalid_jsonQuotes, ordersThe body did not parse as JSON.
invalid_requestQuotes, orders, order lookupSchema validation failed, or a clientOrderId arrived without an externalUserId.
missing_userQuotes, orders, deposit addressNo X-Predicta-User header.
invalid_cursorEvery cursored readMalformed, or issued under a different sort/dir. Restart from the first page rather than returning a confidently wrong slice.
invalid_limitEvery paged readOutside the route’s range.
invalid_categoryEventsNot a canonical category. Refused rather than ignored: a bad filter that returns the whole catalogue is a bug that looks like working software.
invalid_sortEventsNot one of trending, volume, closing-soon, new.
invalid_dirEventsNot asc or desc.
invalid_dateEventsclosesAfter / closesBefore were not ISO-8601.
invalid_tradableEventstradable was not true or false.
invalid_typeReconciliation feedUnknown item type in a ?type= filter.
invalid_fromReconciliationThe window bound was not ISO-8601.
invalid_sinceDeprecated settlement feedNot ISO-8601. Refused rather than treated as “from the beginning”.
invalid_outcomeSettlementsoutcome was not won, lost or void.
invalid_settled_fromSettlementssettledFrom was not ISO-8601.
invalid_settled_toSettlementssettledTo was not ISO-8601.

401 / 403: credentials, scope and eligibility

StatuserrorMeaning
401missing_credentialsNo Authorization header, or not a Bearer token.
401invalid_keyUnrecognised key.
401revoked_keyThe key was revoked.
403insufficient_scopeThe 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.
403operator_suspendedThe operator is suspended.
403sandbox_onlyA balance, ledger or funding endpoint called by a live tenant. Those model a Predicta-held balance and exist for sandbox integration only.
403market_not_offeredThe 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.
403quote_not_yoursThe quote belongs to another operator. A quote is an offer to one party.

404: not found

errorMeaning
market_not_foundNo such contract, on a quote request.
quote_not_foundNo such quote, on an order request.
unknown_eventNo such event id or slug in this operator’s catalogue.
unknown_orderNo 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_positionSame rule, for a position.
unknown_userNo such player for this operator. Reads never create one, so check the id before retrying.
unknown_depositNo such deposit for this operator.

409: the state moved

errorMeaningWhat to do
price_staleThe 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_expiredThe quote passed expiresAt.Re-quote and re-confirm the new price.
quote_already_usedThat 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_requiredThe 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

errorMeaning
market_not_openThe contract has closed, resolved or been voided. detail carries the status.
no_priceNo usable price for that side. Show “no price”.
invalid_stakeThe stake buys zero contracts.
stake_exceeds_stale_limitThe price is stale enough to cap the stake. The response carries maxStake.
market_unavailableThe contract closed between quote and order.
no_eligible_venueNo source is configured to execute this contract. A configuration problem: it does not resolve itself on the next tick.
insufficient_positionThe sell is larger than the position held. Size the exit in contracts from the position you read back.
insufficient_collateralThe execution layer has no collateral available for this order.
rejectedThe execution layer refused. detail carries the reason.
route_not_supportedFunding only. That asset and chain pair is not in the live catalogue.

429, 5xx

StatuserrorMeaning
429rate_limitedOver the per-key allowance. The body carries retryAfterSeconds and the response adds Retry-After.
500internal_errorAn unhandled failure. Retry with backoff; report it if it persists.
502bridge_unavailableFunding only. The bridge could not be reached; nothing was recorded, so retry is safe.
503funding_unavailableFunding only. No treasury destination configured. Not retryable without an operations change.
503no_address_for_chainFunding 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.

CodeStatus
422 insufficient_balanceNot 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_conflictNot 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_unavailableNot 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

typescript
// 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 when settlementState is settled. If you never received an orderId, find it with GET /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 required on an insufficient_scope and issue a key that carries it. Waiting fixes nothing.
  • fix: never retry unchanged. A 422 retried 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.