Skip to main content

Error Handling

Every API response uses standard HTTP status codes with structured JSON error bodies.

Error Response Format

By default, the /v1/* surface returns PolySimulator’s stable two-field envelope: error holds the machine-readable code and message holds the human-readable description.
For stable error handling, branch on error or the identical X-Polysim-Code header, never on message. The X-Request-Id header echoes the request id for log correlation. This is a documented PolySimulator extension to Polymarket’s single-field {"error": "<message>"} shape. The runtime envelope did not change when this documentation was corrected on 2026-08-29. 402 UPGRADE_REQUIRED is the one enriched response: its body also adds feature_key and upgrade_url so SDKs can render an upsell flow without an extra round-trip.
For the runtime allowlist gate (ACCESS_RESTRICTED — an already-issued key whose account isn’t on the API v1 allowlist, or is flagged / under review), the body uses the same two-field envelope:
The residual key-issuance codes use the same shape. Default open beta self-serves keys: every self-serve tier, free included, gets a ["read", "trade"] key (free keys issued on or after 2026-09-05 trade against a $100 non-renewable API wallet; free keys issued before that date stay read-only). 403 TIER_REQUIRES_UPGRADE is the expected refusal when a free caller asks for a paid tier, for admin, or for trade while holding an expired paid grant. CLOSED_BETA / API_PRO_COMING_SOON remain valid residual codes for an emergency close. The feature_key / upgrade_url hints are body fields only on 402 responses, not these 403s. See Open Beta Errors below.
Verbose body opt-in. Send X-Polysim-Verbose: true on any request to add diagnostic fields:

HTTP Status Codes


Common Error Codes

Trading Errors

This table is the canonical trading error-code reference — other trading pages link here rather than restating the codes.
There is no 409 LIMIT_PRICE_NOT_MET, 409 IDEMPOTENCY_CONFLICT, CANNOT_CANCEL, or HTTP_409 trading code — those names appeared in earlier drafts but are not emitted by the engine. Use the codes above.

Deadline, busy, and concurrency errors

Order writes are serialized per user, not per API key. A second write waits briefly for the first; if the lock is still held, it returns 409 TRADE_IN_PROGRESS with Retry-After: 1. Separate keys for the same user do not bypass this lock. Wait, then retry the same logical request with the same client_order_id.

Order status values

OrderResponse.status (and the status filter on GET /v1/orders) use the PolySimulator-native enum — note the double-L CANCELLED: SDKs ported from Polymarket read the PM-shape ORDER_STATUS_* enum from GET /v1/data/orders instead — where the cancelled member is the single-L ORDER_STATUS_CANCELED (PM’s exact spelling). The native GET /v1/orders path uses double-L CANCELLED; the PM-shape GET /v1/data/orders path uses single-L ORDER_STATUS_CANCELED. See CLOB Compatibility.

No naked shorts — INSUFFICIENT_POSITION explained

PolySimulator (and Polymarket itself) requires every SELL order to be backed by an existing position in that exact token_id. There is no margin, no borrow, no synthetic short. If you try to sell shares you don’t hold, the order is rejected with 400 INSUFFICIENT_POSITION (header X-Polysim-Code: INSUFFICIENT_POSITION). For binary markets (every /markets/{id} with two outcomes), the standard market-maker idiom is a two-sided buy rather than a buy + a short:
After a buy fills you accumulate position; subsequent SELLs against that position are accepted up to the held quantity. Use GET /v1/account/positions to check your inventory per token_id before submitting a SELL.

Authentication Errors

Rate Limit Errors

The backend emits two distinct 429 codes (in the X-Polysim-Code header). Both are retryable and both carry Retry-After — branch on either, or simply treat any 429 as a back-off signal:
A bot that branches only on RATE_LIMIT_EXCEEDED will miss the RATE_LIMITED 429s from the trade-write paths (and vice-versa). The robust pattern is to back off on resp.status_code == 429 regardless of which code is in the header — and to honour Retry-After, which for FREE_TIER_DAILY_LIMIT is hours, not a second.
The per-tier limits (authoritative source: GET /v1/keys/tiers): Legacy cohort keys (if any remain) run at the enterprise tier until their beta_until cutoff, then auto-downgrade to free + read-only. If a static value here ever disagrees with GET /v1/keys/tiers, the endpoint wins.

Open Beta Errors

Default open beta is self-serve. Key issuance is not waitlist-gated: Legacy beta-issued keys carry an X-API-Beta-Cutoff response header on every request after the cutoff date — SDKs can pivot to read-only mode without an extra round-trip.

Retry Strategy


WebSocket Error Handling

WebSocket connections use custom close codes:

Best Practices

Always check status codes

Never assume a 2xx response. Parse the status code and handle each category appropriately.

Use Retry-After header

On 429 responses, the Retry-After header tells you exactly how long to wait. Don’t guess.

Don't retry 4xx errors

Client errors (400-422) indicate a problem with your request. Fix the payload instead of retrying.

Log error details

Always log the full error response body for debugging — the details field often contains actionable info.