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 |
| Scale | 10,000 / min | 25 | 500 |
| Enterprise | negotiated | negotiated | negotiated |
The window is a rolling 60 seconds per API key, not per account and not per endpoint. 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”With no server-supplied delay, use exponential backoff with jitter. The window is 60 seconds, so 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 retries GET requests once on 429 by default
(retryOn429). 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, the API fails open for reads — requests
are served rather than rejected, and credits remain the spend backstop. Side-effecting
routes fail closed with 503 PLATFORM_UNAVAILABLE instead, because replaying an
uncertain write is worse than refusing it.
No special handling is required. It is documented so that a burst of 503s on writes
while reads continue is recognisable as designed behaviour rather than a partial outage.