Rate limits

Requests are limited per account. The default budget is 60 requests per minute, and every response tells you where you stand — so a well-behaved client paces itself instead of discovering the ceiling by hitting it.

One account, one budget. Every key on your account draws on the same allowance, and that includes test-mode keys: minting more keys does not mint more budget. X-RateLimit-Limit is therefore the ceiling actually enforced for you, whichever key a request arrives on — which is what makes X-RateLimit-Remaining safe to pace against. If you need more headroom, the account limit itself is what gets raised — and raising it raises the ceiling you can actually reach, because the pre-authentication guard described below never meters a request that authenticates.

On this page

Response headers

Header On Value
X-RateLimit-Limit every budgeted response Requests allowed per window (one minute)
X-RateLimit-Remaining every budgeted response Requests left in the current window, already counting the response carrying the header. 0 on a rejection
X-RateLimit-Reset every budgeted response Absolute Unix timestamp in seconds — the instant the current window ends
Retry-After 429 only Whole seconds to wait, per RFC 9110. Never 0

X-RateLimit-Reset is an instant, not a delay. It is identical on every response within the same window, so a client that queues work does not have to remember when it read the header:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1750000080

Retry-After on a 429 counts whole seconds to that same instant and is clamped to at least 1 — a sub-second remainder still means "wait", and 0 would read as "retry immediately".

Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1750000080

The simplest correct client sleeps for Retry-After seconds on a 429, and otherwise slows down when X-RateLimit-Remaining gets low.

429 response

{
  "status": 429,
  "code": "rate_limited",
  "error": "Rate limit exceeded."
}

Branch on code: "rate_limited", not on the message. The full envelope is described in Errors.

Three responses that carry no budget headers

  • A 401 from authentication. No key was resolved, so there is no account and no budget to report.
  • A 403 insufficient_scope. The scope check runs before the budget is spent, so a request your key is not permitted to make never costs you a request from your per-account allowance. It is not free of every limit, though: repeated denials count against the coarse anti-flood guard below, so a retry loop against an endpoint your key is not scoped for eventually gets a 429.
  • GET /api/v1/events/stream. The event stream is deliberately outside the per-account budget: a reconnect loop would otherwise burn a per-minute allowance and cost you your event feed exactly when your integration is misbehaving. It is therefore never rate-limited and reports no budget.

A separate coarse anti-flood guard sits in front of authentication and applies to every /api/v1 request, the stream included. It exists to stop a flood of invalid keys from driving database lookups, and it counts only requests your credential could not carry out — one that fails to authenticate, or one refused by the scope check. A request that authenticates and is permitted spends nothing from it, whatever your account's limit is.

Why the two 429s can report different limits

There are two limiters, and each 429 reports the ceiling that actually rejected you:

Counts Partitioned by Ceiling
Your per-account budget Every authenticated and permitted request Your account X-RateLimit-Limit on your successes — raisable
Coarse anti-flood guard Only requests your credential could not carry out: it failed to authenticate, or the scope check refused it The key you presented, or your IP if you sent none A fixed server-side number, unrelated to your account's

For a working integration, only the first row can ever reject you: the per-account budget is the only ceiling a caller doing permitted work can reach, at any value. You meet the guard only while you are sending a key that does not resolve — a stale or revoked credential in a retry loop, say — or one that keeps asking for something it is not scoped for. Its 429 then reports its own ceiling rather than yours. That is not a contradiction: the two numbers describe two different buckets.

Note that the guard's bucket is per presented key, and it is checked before the request is dispatched. So a key that has spent that bucket on refused requests is briefly throttled on its permitted routes as well, until the guard's one-minute window rolls over. Your account's other keys are unaffected.

One consequence worth knowing if you are debugging: because the guard runs before any key is resolved, its rejections cannot be attributed to an account and so do not appear in your request log (Member → API → Logs). Every 429 from your per-account budget does.

The window is fixed, not sliding

The budget resets on a fixed one-minute boundary rather than sliding. That has one consequence worth knowing: you can send up to twice the limit across a boundary — a full window's worth in the last second of one window and another full window's worth in the first second of the next. This is accepted and documented rather than smoothed away; if you are pacing near the ceiling, spread requests across the window rather than bursting at its edges.

One related detail: if your account's per-minute limit is changed mid-window, your current window restarts. It is a rare, administrative event and it only ever gives you budget back.

Handling limits well

  • Read X-RateLimit-Remaining and throttle before you are rejected.
  • On a 429, sleep for Retry-After seconds, then retry. Add jitter if you run many workers, so they do not all wake at the same instant.
  • Retrying a credit-spending POST after a timeout? Send an Idempotency-Key so the retry cannot charge you twice. See Idempotency.
  • For high volume, prefer batch validation (POST /api/v1/verify/batch): one request uploads an entire file, so a million addresses cost one request against your budget rather than a million.