Skip to content

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.

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.

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: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1760789340

X-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.

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.

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.

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.

  • Read the headers on every response, including successful ones, and track remaining budget.
  • On 429, honour details.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.