Rate limits

Two independent limiters run on the API:

| Surface | Bucket | Limit | |---|---|---| | Authenticated endpoints (/api/v1/* with a key) | Per API key | 60 requests / rolling 60s | | GET /verify/:code (public) | Per caller IP | 60 requests / rolling 60s |

Limits are per-key (or per-IP), not per-endpoint — every call your key makes shares the same 60-request window. The public verify limiter is separate: it applies even though the endpoint takes no key, so a verification widget embedded on your site is limited by the visitor's IP, not by your account.

Headers

Every /api/v1/* response carries the current window state — authenticated responses and public /verify/:code responses alike:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1735689660

X-RateLimit-Reset is a Unix timestamp (seconds) marking the end of the current window.

429 responses

When you exceed the limit, you get:

HTTP/1.1 429 Too Many Requests
Retry-After: 27
Content-Type: application/json

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Slow down and try again."
  }
}

Wait Retry-After seconds before retrying. Implement exponential backoff with jitter if you're batching high volumes.

Best practices

  • Batch issuance. Use recipients: [...] (up to 500 per call) instead of looping recipient: singletons. One batch call costs one request against your limit.
  • Cache reads. GET /templates and GET /templates/:id responses are stable — cache them in your integration.
  • Cache verification results. If you render a public verification page, cache by code rather than calling /verify/:code on every page view — the IP limiter is shared across all your visitors behind a proxy.
  • Respect Retry-After. Hammering during rate-limit windows extends the block.

Need higher limits?

Reach out via contact with your use case and expected RPS.