Rate limits
Every /v1 endpoint is guarded by a fixed-window rate limiter keyed by the authenticated API key.
Limits are per key, not per workspace, so each integration gets its own budget.
Default budget
Section titled “Default budget”| Setting | Value |
|---|---|
| Window | 60 seconds, fixed |
| Requests per window | 120 per API key |
| Scope | Per key id |
| Override | PUBLIC_API_RATE_LIMIT (positive integer, operator-set) |
A fixed window means the counter resets at a boundary, not on a sliding basis. A burst at the end of one window and the start of the next can therefore be up to twice the per-window limit.
Response headers
Section titled “Response headers”Every /v1 response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed in the current window |
X-RateLimit-Remaining |
Requests left after this one (never below 0) |
X-RateLimit-Reset |
Window reset time as a Unix timestamp in seconds |
X-RateLimit-Limit: 120X-RateLimit-Remaining: 117X-RateLimit-Reset: 1760789340X-RateLimit-Reset is an absolute epoch second, not a duration. Convert it with
new Date(reset * 1000) rather than adding it to the current time.
When the limit is exceeded
Section titled “When the limit is exceeded”HTTP/1.1 429 Too Many Requests{ "error": { "code": "rate_limited", "message": "Rate limit exceeded", "details": { "limit": 120, "retry_after_seconds": 17 } }, "request_id": "2c9e4f10-8a7b-4d3c-9e5f-1a2b3c4d5e6f"}The header block is still present on the 429, with X-RateLimit-Remaining: 0. Wait at least
details.retry_after_seconds before retrying.
Verification-specific limits
Section titled “Verification-specific limits”Creating a verification (POST /v1/verifications) also passes through two product-level limits
that protect destinations and your business from OTP abuse:
| Limit | Default | Window | Env override |
|---|---|---|---|
| Requests to one destination | 5 | 900 seconds | VERIFY_RATE_DEST_LIMIT, VERIFY_RATE_WINDOW_SECONDS |
| Requests per business | 20 | 900 seconds | VERIFY_RATE_BIZ_LIMIT, VERIFY_RATE_WINDOW_SECONDS |
These counts include every channel and are checked before the code is sent. Exceeding either
returns 429 with a reason in details:
{ "error": { "code": "rate_limited", "message": "rate_limited", "details": { "reason": "destination", "retry_after_seconds": 900 } }, "request_id": "2c9e4f10-8a7b-4d3c-9e5f-1a2b3c4d5e6f"}reason is destination or business. Unlike the request budget, these do not emit
X-RateLimit-* headers.
Multi-replica behaviour
Section titled “Multi-replica behaviour”The request-budget counter is held in process memory on each API instance. Behind multiple replicas, each replica enforces its own 120-request window, so the aggregate ceiling across the deployment can exceed 120 per minute. Plan for the per-instance figure and treat 120/minute as a floor rather than a hard global quota.
Handling limits well
Section titled “Handling limits well”- Read the headers on every response, including successful ones, and track remaining budget.
- On
429, honourdetails.retry_after_seconds; add jitter to avoid a synchronized retry storm. - Retry only after the window resets. Rate-limited requests are not stored by the idempotency layer, so the retry executes normally.
- Prefer webhooks over polling for state changes — see Webhooks.
- Split unrelated workloads across separate keys if one workload would starve another.