GuidesResources
Rate limits & retries
Every API key is rate limited per plan. Exceeding the window returns 429 RATE_LIMITED
with retryable: true in the standard error envelope.
Rate limiting and credits are separate controls. The limiter caps how
fast you may call; credits cap how much you may call in total. A request can pass the
limiter and still fail with 402 INSUFFICIENT_CREDITS, or fail the limiter without ever
being charged.
Limits by plan
Section titled “Limits by plan”| Plan | Requests | API keys | Concurrent WebSocket streams |
|---|---|---|---|
| Free | 60 / min | 1 | 2 |
| Builder | 300 / min | 3 | 20 |
| Pro | 3,000 / min | 10 | 100 |
| Enterprise | negotiated | negotiated | negotiated |
The limiter uses a fixed 60-second window per API key, not per account and not per endpoint. Requests on both sides of a bucket boundary can arrive back-to-back. Two keys on one account each get the full allowance; one key spread across ten processes shares a single allowance.
Enterprise plans and individual keys can carry a bespoke override. An override is applied as a per-second window rather than per-minute — so a key provisioned at 50/s is allowed 50 in any given second, not 3,000 spread freely across a minute.
What the response tells you
Section titled “What the response tells you”{ "success": false, "error": { "code": "RATE_LIMITED", "message": "rate limit exceeded — retry shortly", "retryable": true }}Backing off
Section titled “Backing off”Honor the server-supplied Retry-After delay. If that delay is unavailable to your retry wrapper,
use exponential backoff with jitter. A client retrying every second spends its next allowance on
failures.
async function withRetry<T>(call: () => Promise<T>, attempts = 5): Promise<T> { for (let attempt = 0; ; attempt++) { try { return await call(); } catch (err) { const code = (err as { code?: string }).code; // Only these are worth retrying. A 400 or 401 will fail identically forever. if (code !== 'RATE_LIMITED' && code !== 'PLATFORM_UNAVAILABLE') throw err; if (attempt >= attempts - 1) throw err; // 1s, 2s, 4s, 8s … plus jitter so parallel workers do not resynchronise. const backoff = 2 ** attempt * 1000 + Math.random() * 1000; await new Promise((r) => setTimeout(r, backoff)); } }}The TypeScript SDK and the Python SDK retry GET requests once on 429 by
default (retryOn429 / retry_on_429). A 429 whose envelope says retryable: false is surfaced
immediately: the flag is authoritative and the SDKs never retry against it. Writes are never
auto-retried — a submit or cancel that may have
reached the venue must not be replayed by a client library. For those, retry deliberately
and send an Idempotency-Key; see Trading & execution.
Which errors to retry
Section titled “Which errors to retry”| Code | HTTP | Retry? |
|---|---|---|
RATE_LIMITED | 429 | Yes — back off, then retry |
PLATFORM_UNAVAILABLE | 503 | Yes — back off, then retry |
CATALOG_UNAVAILABLE / HISTORY_UNAVAILABLE | 503 | Yes — back off, then retry |
INSUFFICIENT_CREDITS | 402 | No — retrying cannot succeed until the balance changes |
VALIDATION_ERROR | 400 | No — fix the request |
UNAUTHORIZED | 401 | No — fix the key |
NOT_SUPPORTED | 400 / 501 | No — an honest capability gap, not a failure |
retryable is present on every error and is the field to branch on. Treat it as
authoritative over the HTTP status.
Staying under the limit
Section titled “Staying under the limit”- Prefer cursors over parallel offset pages. Following
nextCursorkeeps one request in flight; twenty parallel offset pages spend twenty of the allowance in one second. - Batch where a batch verb exists.
fetchOrderBookstakes many outcomes in one request. Note it is priced by items, so it saves allowance rather than credits. - Stream instead of polling. A WebSocket subscription delivers book and trade updates without consuming the request window at all. Polling a book every second on Free spends the entire minute allowance on one market.
- Cache what does not move. Venue capability maps (
has) and taxonomy (fetchCategories,fetchTags) change rarely; re-fetching them per request is pure overhead. - Spread scheduled work. Offset cron jobs by a random delay so batch runs do not collide with each other or with interactive traffic.
When rate limiting itself is degraded
Section titled “When rate limiting itself is degraded”If the limiter’s backing store is unreachable, most routes fail open at this layer and credits
remain the spend backstop. Only the zero-credit billing-session routes — checkout, subscribe, and
portal — fail closed with 503 PLATFORM_UNAVAILABLE. This prevents unlimited Stripe sessions while
the limiter is unavailable; other writes, including webhook management, are not in that guarded set.
No special handling is required. A burst of 503s on those billing-session routes while other
traffic continues is recognisable as designed behaviour rather than a partial outage.