Skip to content

GuidesResources

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.

PlanRequestsAPI keysConcurrent WebSocket streams
Free60 / min12
Builder300 / min320
Pro3,000 / min10100
Enterprisenegotiatednegotiatednegotiated

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.

{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "rate limit exceeded — retry shortly",
"retryable": true
}
}

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.

CodeHTTPRetry?
RATE_LIMITED429Yes — back off, then retry
PLATFORM_UNAVAILABLE503Yes — back off, then retry
CATALOG_UNAVAILABLE / HISTORY_UNAVAILABLE503Yes — back off, then retry
INSUFFICIENT_CREDITS402No — retrying cannot succeed until the balance changes
VALIDATION_ERROR400No — fix the request
UNAUTHORIZED401No — fix the key
NOT_SUPPORTED400 / 501No — 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.

  • Prefer cursors over parallel offset pages. Following nextCursor keeps one request in flight; twenty parallel offset pages spend twenty of the allowance in one second.
  • Batch where a batch verb exists. fetchOrderBooks takes 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.

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.