By default, the /v1/* surface returns the Polymarket-shape
single-field envelope — one error key holding a human-readable
description:
{"error": "Account balance $12.50 insufficient for order cost $25.00"}
Field
Type
Description
error
string
Human-readable description. For PM-shape parity, the machine-readable short code is carried out-of-band in the X-Polysim-Code response header (e.g. INSUFFICIENT_BALANCE, RATE_LIMIT_EXCEEDED).
For stable error handling, branch on the X-Polysim-Code header, not
on the body text — the body holds the prose, the header holds the code.
The X-Request-Id header echoes the request id for log correlation.402 UPGRADE_REQUIRED is the one carve-out to the PM-shape default:
its body adds feature_key and upgrade_url alongside error so SDKs
can render an upsell flow without an extra round-trip. Every other
status code (401, 403, 404, 429, 5xx, …) sticks with the single-field
shape and exposes the machine code via X-Polysim-Code.
// 402 UPGRADE_REQUIRED — feature_key + upgrade_url at the root{ "error": "UPGRADE_REQUIRED", "feature_key": "wallets.sandbox_baseline", "upgrade_url": "/pricing"}
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 is PM-shape and the code is in the header:
// 403 ACCESS_RESTRICTED (not on the allowlist / flagged account)// + headers: X-Polysim-Code: ACCESS_RESTRICTED{"error": "API access restricted. Contact support to request access."}
The closed-beta key-issuance gate uses the same shape: POST /v1/keys
and POST /v1/keys/bootstrap return 403 CLOSED_BETA (the default for
every non-admitted caller, including paying Pro / Pro+; the
API_PRO_COMING_SOON variant appears only once self-serve issuance is
enabled) with the machine code in the X-Polysim-Code header and the
human message in the body’s error field. The feature_key /
upgrade_url hints are body fields only on 402 responses, not these
403s. See Closed Beta Errors below.
Verbose body opt-in. Send X-Polysim-Verbose: true on any
request to get the legacy multi-field shape:
{"error": "INVALID_KEY", "message": "Invalid API key", "details": null, "request_id": "a1b2c3d4-..."}
Useful when writing or debugging an SDK; PM-shape is the default so
Polymarket-CLOB SDK ports work without translation.
This table is the canonical trading error-code reference — other
trading pages link here rather than restating the codes.
Error Code
HTTP
Description
INSUFFICIENT_BALANCE
400
Not enough funds for order
INSUFFICIENT_POSITION
400
Sell rejected — your position size for the chosen token_id is smaller than the requested quantity. PolySimulator does not support naked shorts: every SELL must be backed by an existing position. See No naked shorts below for the two-sided-quote workaround.
INVALID_QUANTITY
400
Non-positive quantity rejected by the execution engine. (A schema-level out-of-range value is caught earlier by Pydantic as 422 VALIDATION_FAILED — see HTTP Status Codes above.)
PRICE_REQUIRED
400
Market order submitted without the required price (worst-price limit).
FOK_ORDER_NOT_FILLED_ERROR
400
The order could not fill entirely at or beyond your worst-price limit (BUY: best price above your cap; SELL: best price below your floor). This is the worst-price rejection for market / FOK orders — there is no409 LIMIT_PRICE_NOT_MET code.
INVALID_ORDER_PAYLOAD
400
Body shape invalid (PM-shape /v1/order): bad maker/taker amounts, missing tokenId, or unsupported side
INVALID_ORDER_MIN_TICK_SIZE
400
Limit price doesn’t conform to the market’s tick size (0.1 / 0.01 / 0.001 / 0.0001). Round to a multiple of GET /v1/tick-size/{token_id}. Mirrors Polymarket’s behaviour exactly.
UNSUPPORTED_ORDER_TYPE
400
order_type=IOC on POST /v1/clob/order is rejected (use GTC to rest or FOK for immediate-or-fail). Code is in the X-Polysim-Code header.
MARKET_NOT_FOUND
404
Unknown market_id (or unknown tokenId on PM-shape /v1/order). Verify with GET /v1/markets-by-token/{token_id}.
MARKET_CLOSED
400
Market is resolved or inactive
ORDER_NOT_FOUND
404
Unknown order_id
ORDER_NOT_CANCELLABLE
400
The order is already FILLED, CANCELLED, or EXPIRED and can’t be cancelled.
DUPLICATE_CLIENT_ORDER_ID
409
A new order reused a client_order_id already bound to a different order.
IDEMPOTENCY_KEY_REUSE
409
The same Idempotency-Key was replayed with a different request body. (An identical replay instead returns the original order — see Idempotency below.)
IDEMPOTENCY_CONFLICT_PENDING
409
The same Idempotency-Key is still being processed; carries Retry-After: 1.
EXECUTION_ERROR
500
Server-side error during fill — report with request_id
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.
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:
# Wrong — naked short on the "Down" side:sell_down = post("/v1/order", json={"token_id": down_tok, "side": "SELL", "quantity": 100, "price": 0.51})# → 400 INSUFFICIENT_POSITION (you don't hold any Down shares)# Right — buy both sides instead. Up + Down ≈ 1.00, so total notional is similar# to a two-sided quote, and you keep the spread on whichever side fills first:buy_up = post("/v1/order", json={"token_id": up_tok, "side": "BUY", "quantity": 100, "price": 0.49})buy_down = post("/v1/order", json={"token_id": down_tok, "side": "BUY", "quantity": 100, "price": 0.49})
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.
No Authorization: Bearer … or X-API-Key on a route that accepts either
MISSING_API_KEY
401
X-API-Key (or legacy POLY_API_KEY) missing on a key-only route
INVALID_KEY
401
Key doesn’t exist or is revoked
INVALID_TOKEN
401
Bearer JWT is malformed, missing sub, or fails signature verification
KEY_EXPIRED
401
API key has expired
KEY_DEACTIVATED
401
Key was administratively disabled
TOKEN_EXPIRED
401
Bearer JWT past its exp claim
INSUFFICIENT_PERMISSION
403
Key missing the required permission (e.g. trade for order endpoints)
ACCESS_RESTRICTED
403
An already-issued key (or Bearer JWT) is not on the admin-managed API v1 allowlist (or the account is flagged / under review). Returned on authenticated /v1/* requests. Body is PM-shape; check the X-Polysim-Code header.
CLOSED_BETA
403
Key issuance refused on POST /v1/keys / POST /v1/keys/bootstrap. The default for every non-admitted caller while the beta is closed, including paying Pro / Pro+. Machine code in the X-Polysim-Code header; the body’s error is the human message.
API_PRO_COMING_SOON
403
Conditional variant of the issuance gate for a paying Pro / Pro+ caller without a cohort grant — reached only once self-serve issuance is enabled (until then they also get CLOSED_BETA). Machine code in the X-Polysim-Code header.
UPGRADE_REQUIRED
402
Pro-tier cap reached (sandbox count, etc.). Only 402 carries feature_key + upgrade_url at the root of the response body.
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:
Error Code
HTTP
Description
RATE_LIMIT_EXCEEDED
429
The per-tier in-process concurrency cap (all /v1/* paths) or the IP / per-key request-rate limiter. Retry after Retry-After.
RATE_LIMITED
429
The cross-worker trade-concurrency limiter on the three trade-write paths (POST /v1/orders, /v1/orders/batch, /v1/clob/order). Body also carries retry_after_ms. Retry after Retry-After.
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.
The per-tier limits (authoritative source: GET /v1/keys/tiers, seeded
from the ApiRateLimit ladder):
Tier
Req/sec
Req/min
WS conns
Max batch
Free
2
120
1
1
Pro
10
600
3
5
Pro+
30
1,800
10
10
Enterprise
100
6,000
50
25
Closed-beta cohort keys run at the enterprise tier until the cutoff
(2026-08-31), then auto-downgrade to free + read-only. If a static value
here ever disagrees with GET /v1/keys/tiers, the endpoint wins.
The API is in an ongoing closed beta. Key issuance is cohort-gated:
Error Code
HTTP
Description
CLOSED_BETA
403
POST /v1/keys / POST /v1/keys/bootstrap refused. The default for every non-admitted caller while the beta is closed, including paying Pro / Pro+. Machine code in the X-Polysim-Code header; the body’s error is the human message. Not retryable — apply via the waitlist at https://polysimulator.com/api-trading.
API_PRO_COMING_SOON
403
Same endpoints, conditional variant for a paying Pro / Pro+ caller without a cohort grant — reached only once self-serve issuance is enabled (until then they also get CLOSED_BETA). Machine code in the X-Polysim-Code header.
ACCESS_RESTRICTED
403
Separate runtime gate: an already-issued key (or Bearer JWT) is not on the admin-managed allowlist (or is flagged / under review). Returned on authenticated /v1/* requests. Check the X-Polysim-Code header. Not retryable.
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.
# Handle the closed-beta key-issuance gate cleanly. The issuance codes# (CLOSED_BETA / API_PRO_COMING_SOON) are in the `X-Polysim-Code` header;# the body's `error` field holds the human message.resp = requests.post(f"{BASE_URL}/v1/keys", headers={"X-API-Key": KEY}, json={"name": "bot"})if resp.status_code == 403: code = resp.headers.get("X-Polysim-Code") if code == "CLOSED_BETA": # Not in an admitted cohort yet — apply via the waitlist. print("Closed beta — join the waitlist at https://polysimulator.com/api-trading") elif code == "API_PRO_COMING_SOON": # Paying Pro / Pro+ without a cohort grant — rolling out in cohorts. print("Pro API access is rolling out — see /account/billing")