Reference

Rate limits

A per-key, per-minute limiter, counted in the database rather than in a process, applied to every /api/v1 route by the same gate that checks the key.

Status

Scope per API keyWindow 60s fixedSandbox default 120/minRateLimit headers every response

Every /api/v1 request passes through one gate, which authenticates the key, checks the route's scope, meters the call and resolves the tenant's mode. Nothing is exempt, so there is no route where a client can skip the budget.

EndpointScope
GET /api/v1/eventsmarkets:read
GET /api/v1/events/{id}markets:read
GET /api/v1/categoriesmarkets:read
POST /api/v1/quotesquotes:write
POST /api/v1/ordersorders:write
GET /api/v1/ordersorders:read
GET /api/v1/orders/{id}orders:read
GET /api/v1/positionspositions:read
GET /api/v1/positions/{id}positions:read
GET /api/v1/settlementssettlements:read
GET /api/v1/settlements/positionssettlements:read — deprecated
GET /api/v1/reconciliationledger:read
GET /api/v1/reconciliation/feedledger:read
GET /api/v1/users/{id}/balanceledger:read — sandbox only
GET /api/v1/users/{id}/ledgerledger:read — sandbox only
GET /api/v1/funding/supported-assetsfunding:write — sandbox only
POST /api/v1/funding/deposit-addressfunding:write — sandbox only
GET /api/v1/funding/deposits/{id}funding:write — sandbox only
GET /api/v1/images/{id}No key, no scope, not metered: it is an <img> tag in a browser.

That is the whole /api/v1 surface — eighteen paths, nineteen operations. Nothing else exists on it.

How the limit works

PropertyValue
ScopePer API key, not per operator and not per IP.
WindowFixed, 60 seconds, aligned to the wall clock minute.
Sandbox default120 requests per minute.
Configured onThe key itself, so a sandbox key and a live key can differ.
CounterA row in Postgres, incremented by a single upsert that returns the new count.

Scope is checked before the limit is consumed

A caller with the wrong scope has made a mistake that no amount of waiting fixes. Charging them a request to say so would turn the 403 into a 429 on the retry, which reads as an entirely different bug and sends whoever is debugging it to this page instead of to their key configuration.

Why the count lives in the database

A per-process counter multiplies the real limit by however many instances are running, which is a limit in name only. A single upsert returning the new count also means two concurrent requests cannot both read the same number and both decide they were under.

A fixed window admits a burst

Twice the allowance can pass across a window boundary. That is a known and accepted property: this protects the service from a runaway loop, not from an adversary. Do not design around the burst — it is not a feature and the window shape may change.

What every response tells you

Every response carries the current window on three headers — a success as much as a refusal — so a client can slow down before it is refused rather than discovering the budget by hitting it.

http
RateLimit-Limit: 120
RateLimit-Remaining: 96
RateLimit-Reset: 37

When the allowance is spent the request is refused with 429, a Retry-After header, and a body that repeats the wait in seconds:

429 application/json
{
  "error": "rate_limited",
  "message": "Over 120 requests per minute for this key.",
  "retryAfterSeconds": 37
}
  • RateLimit-Limit: the allowance for this key, this window.
  • RateLimit-Remaining: floored at zero, so it never goes negative on a burst.
  • RateLimit-Reset, Retry-After and retryAfterSeconds: seconds until the window rolls. Sleep that long, not a fixed guess.
  • Read the headers on successful responses too. Backing off as RateLimit-Remaining approaches zero is how you avoid the 429 entirely.

The SDK raises a RateLimitError carrying retryAfterSeconds, so the wait is already parsed for you.

Staying inside the budget

Most integrations that hit a limit are doing one of four things.

Instead ofDo this
Refetching the whole catalogue on a timerPage it once with ?cursor=, then re-read narrowly — ?closesAfter= and ?tradable=true — rather than pulling every event to find the handful that changed.
Polling the settlement register continuouslyPoll on the order of once a minute, carrying nextCursor. Settlements are not high-frequency events.
Re-quoting on a timer to keep a ticket warmQuote when the player is about to act. A quote carries expiresAt; re-quote on expiry, not on an interval.
Polling every open order for a fillThe order response already carries the fill. The only order worth polling is one whose settlementState is pending, and the reconciliation feed reports every money fact in one resumable stream.

Retry with jitter

If every client in a fleet sleeps for exactly retryAfterSeconds, they all return at the same instant and the next window is exhausted immediately. Add a random fraction of a second.