Error codes
Every error response carries the envelope that the Getting started page describes. The error field is one of the codes below. We regenerate the table from our error catalog at every build. It lists the codes that an operator can observe.
Branch on the code, not on the message text. We never rename a code and we never change its meaning. We can add new codes without notice. If you receive a code that you do not know, act on the HTTP status class of the response.
The source field is an opaque value. Do not try to decode it. Quote it with the request_id in a support request.
Some codes carry a value in details that tells you which case fired: reason for market_suspended and limit_type for stake_limit_exceeded. The table lists the values under each code. These sets are open: we can add a value without a new API version. If you receive a value that you do not know, act on the code and its HTTP status alone, and do not fail the request handling.
A code with widget in the Status column never appears in an HTTP response. It is a state of the widget in the browser. We list it so that you can understand the widget states that your end users report.
| Status | Code | When it fires | Retry and action |
|---|---|---|---|
| 400 | validation_failed | The request body or query is malformed. On the position routes it also fires for an unknown market or outcome (a market the tenant has not enabled reads as unknown), for a market that is not open, and for a request outside the trading window of the market. An open or an order also stops when the market's cutoff before its close begins; a close does not. A suspended market returns market_suspended instead. | No. Fix the request and send it again. In the widget the market may have moved, so read it again before the next try. |
| 400 | idempotency_key_required | A write request has no Idempotency-Key header. | No. Add the Idempotency-Key header and send the request again. |
| 401 | invalid_api_key | The API key is unknown, revoked or expired. The response does not say which. | No. Get a valid key first: rotate it in the tenant portal or ask the platform team for a new one. Then send the request with it. |
| 401 | invalid_session_token | The widget session token is missing, malformed, expired or revoked. The response does not say which. | Refresh the session with POST /v1/widget/sessions/refresh, then retry the request with the same Idempotency-Key. If the refresh fails with this code too, the host page mints a new session. |
| 403 | insufficient_scope | The API key does not carry the scope that the endpoint needs. It also fires when an anonymous widget session, minted for a visitor who has not logged in, calls a route that acts for a user. | No. Get a key with the scope this endpoint needs, then retry. For an anonymous widget session, mint a session for the user once they log in and pass it to setSession. |
| 403 | tenant_suspended | The tenant is suspended. A suspension also revokes the API keys, so you usually see invalid_api_key instead. | No. The suspension is contractual, not technical. Contact the platform team. |
| 403 | user_blocked | The end user has status blocked. A blocked user cannot open a position and cannot place a limit order. Closing a position, quoting a close and cancelling an order still work. | No. Lift the block with a user upsert (PUT /v1/users), then retry. |
| 404 | not_found | The resource id in the request does not exist. | No. Check the id in the request. |
| 404 | unknown_user | The external_user_id has not been registered with a user upsert. | No. Register the user first with PUT /v1/users, then retry the original request. |
| 409 | would_overdraw | A debit would take the virtual balance of the user below zero. Nothing is applied. | Only after the stake is lowered or the balance of the user changes. |
| 409 | position_terminal | A close was requested for a position that already settled or was voided, or a close quote for one that already settled, was voided or was closed. Nothing is applied. | No. The position is already over, and a retry does not change that. A single closed position has no correction flow. |
| 409 | price_moved | The market price moved past the limit in the request. An open fails above max_price and a close fails below min_price. Nothing is applied. | Quote again from the current price and confirm again. A new open is a new intent with a new Idempotency-Key and a new max_price; a new close carries a new min_price. |
| 409 | market_suspended | The market exists but is suspended. It can reopen. Opening, closing and quoting a position and placing a limit order fail while it is suspended. The same code also comes back when the market is suspended for your account only, after the risk limit of your own pool on it was reached: opening a position and placing a limit order fail and your resting limit orders do not fill, but closing a position works, and other operators trade the market as before. Only an isolated account's own pool is suspended this way. Your owner or admin lifts it from the tenant portal (POST /portal/v1/markets/{id}/risk-suspension/clear); our staff cannot lift it. Values of details.reason today, an open set: | Yes, once the market reopens. The lifecycle events and the Market Feed tell you when it does. A suspension for your account only lasts until your owner or admin lifts it from the tenant portal. |
| 409 | stake_limit_exceeded | The stake is above the per-position cap of the market, or it would take the total open stake of the user on the market above the per-user cap. Nothing is applied. Values of details.limit_type today, an open set: | Only with a lower stake: details.remaining or less. The per-user allowance frees again at settlement, at a void, or when the user closes a position on that market. |
| 409 | stake_below_minimum | The stake is below the per-position minimum of the market. Nothing is applied. The market detail of the feed serves the minimum in stake_limits. | Only with a higher stake. Raise it to stake_limits.min_stake_per_position or more. |
| 409 | tenant_exposure_limit | The open would take the model worst-case loss of your positions on this market above the market's limit. Nothing is applied. Closing a position is never refused for this reason. | Only with a lower stake, or later: the allowance grows when positions on this market close or when the price moves. |
| 409 | price_impact_exceeded | The open would move the outcome's price by more than this market allows in one trade. Nothing is applied. details.max_stake is the largest stake that fits now; a close is never refused for this reason. | Only with a lower stake: send details.max_stake or less. |
| 409 | tenant_limit_reached | Your account reached its monthly loss cap or its credit limit, so new trades are held. Opening a position and placing a limit order fail, and your resting limit orders do not fill. Closing a position, quoting a close and cancelling an order work. A monthly loss cap hold ends when the month ends; a credit limit hold ends when a payment brings your open balance to or under the limit. Either also ends when our staff clear it. | Not until the hold clears. A monthly loss cap hold clears at the start of the next month, a credit limit hold when your open balance is back at or under the limit, and either when our staff clear it. Your portal dashboard shows the hold. |
| 409 | state_conflict | A request to cancel a limit order found the order already filled, expired or rejected. | No. Read the resource again and decide on what it is now. |
| 409 | report_limit_reached | The tenant already has the maximum number of report jobs in status generating. No job is created. | Yes, once one of your report jobs has left status generating. Poll GET /v1/reports/{id} or wait for the report.ready webhook; there is no Retry-After. |
| 409 | saved_markets_limit_reached | The end user already has the maximum number of saved markets, and the market in the request is not one of them. Nothing is saved. Saving a market that is already saved still succeeds. | Only after the user removes a saved market with DELETE /v1/widget/saved-markets/{market_slug}. The widget does not retry on its own. |
| 409 | idempotency_key_in_progress | An earlier request with the same Idempotency-Key is still being processed. Nothing is applied by this request. The response carries a Retry-After header. | Yes. Wait for Retry-After and retry with the same Idempotency-Key and the same body. A new key would be a second intent and can open a second position. |
| 413 | request_too_large | The request body is larger than the size limit of the route. | No. Make the body smaller or split the batch. |
| 422 | idempotency_key_reused | The Idempotency-Key was seen before with a different request body. Nothing is applied. | No. A retry must send the original body byte for byte. A new intent needs a new key. |
| 429 | rate_limited | The tenant or the end user exceeded the rate limit of the route. The response carries a Retry-After header. | Yes. Wait for Retry-After and send the same request with the same Idempotency-Key, so a retried write never applies twice. |
| 500 | internal_error | An unexpected failure on our side. | Yes for a write that carries the same Idempotency-Key. If it keeps failing, send us the request_id and source. |
| 503 | sync_paused | We paused the balance-sync endpoints with a kill switch. | Yes, after we clear the pause. Queued syncs replay safely with their Idempotency-Key. |
| 503 | positions_paused | We paused the opening of positions with a kill switch. | Yes, after we clear the pause. The widget shows a paused state meanwhile. |
| 503 | feed_paused | We paused the Market Feed reads with a kill switch. | Yes, after we clear the pause. |
| 503 | widget_paused | We paused widget session mint, widget session refresh and the widget config read with a kill switch. | Yes, after we clear the pause. |
| 503 | market_not_ready | The state of the market is still rebuilding after a restart on our side. The response carries a Retry-After header. | Yes. Wait for Retry-After and send the same request with the same Idempotency-Key, so a retried open never applies twice. |
| widget | realtime_unavailable | The widget used all its reconnect attempts and now runs on the polling fallback. | Nothing to do. The widget keeps trying to reconnect in the background. |
| widget | widget_session_expired | The widget session token expired and the automatic refresh failed. | No retry from your side. The widget refreshes the session on its own, and this state shows only when that refresh failed. |
| widget | widget_config_invalid | The embed parameters failed the check at widget boot. The widget reports it to the browser console and shows nothing to the end user. | No. Fix the embed parameters. |
| widget | widget_runtime_error | A client-side exception the widget caught at its own boundaries. The widget reports it to us with its error beacon. | No. The widget reports it to us, and there is nothing to retry. |