Skip to content

Rate limits

The public API applies two rate limits: a pre-auth IP limit that defends against brute-force key enumeration, and a per-key limit sharded by route class. Both return rate_limit_exceeded (HTTP 429) when exceeded.

The IP limit runs before authentication — coarse enough to shed abuse without affecting legitimate traffic.

The per-key limit runs after authentication and is sharded by route class so your read traffic does not crowd out your session-create traffic. Every route belongs to exactly one class, and a request counts against that class alone. The classes are:

  • sessions:create: POST /v1/sessions.
  • sessions:read_report: GET /v1/sessions/{id}/report.pdf. Capped well below ordinary reads, because every call renders a document.
  • sessions:read_image: GET /v1/sessions/{id}/idv-image/{artifact}. Flat at 120 requests per minute on every tier, custom included. See Evidence images.
  • idv:create: POST /v1/id-verification, the standalone ID-scan endpoint.
  • applicants:create: POST /v1/applicants, and the applicant update and delete routes.
  • dsar:create: POST /v1/data_subject_requests. Capped low, on a 24-hour window rather than a 60-second one.
  • reads: the list and retrieve reads, for example GET /v1/sessions, GET /v1/sessions/{id}, GET /v1/applicants, GET /v1/workflow-templates and GET /v1/wallet.
  • global: the class for every route that has no more specific class of its own. That includes the verification actions (cancel, resend, notes, skip a step, mint a token) and the workflow-template and link writes.

Each organization has a tier (standard, elevated, enterprise, or custom). New organizations start on standard. Most classes scale with the tier, but three do not move the way you would expect. sessions:read_report is flat from elevated upward, so only the standard to elevated step raises it. sessions:read_image and dsar:create are flat on every tier including custom, so no tier change raises them at all.

Route classes are an internal throttling dimension, independent from the scopes you grant a key: route_class names the traffic bucket a route falls into, not a scope you hold. A key granted only sessions:write still shows route_class: "sessions:create" in a 429 from POST /v1/sessions, since the class name predates a later scope simplification and was kept as-is so existing rate-limit tooling and alerts do not need to change.

Two class names read like scope names and are not. sessions:read_image is a class only: there is no scope by that name, and the scope that authorizes the evidence-image route is sessions:read_report. sessions:read_report is both a class and a scope, and they are still separate things: holding the scope says you may call the route, while the class says which budget the call spends. Grant scopes from the scope reference; never infer one from a route_class you saw in a 429.

Every authenticated response carries the per-key limit state:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 873
X-RateLimit-Reset: 1717000060
X-RateLimit-Window: 60
  • X-RateLimit-Limit — the request budget for the current window.
  • X-RateLimit-Remaining — how many requests are left before 429.
  • X-RateLimit-Reset — UNIX epoch seconds when the window rolls over.
  • X-RateLimit-Window — window length in seconds.

Track X-RateLimit-Remaining and slow down before you reach zero — it is cheaper than handling 429s reactively.

A 429 rate_limit_exceeded response includes details inside the error envelope:

{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded for this API key",
"request_id": "req_...",
"details": {
"reset_at": 1717000060,
"limit": 100,
"window_seconds": 60,
"route_class": "sessions:create",
"tier": "standard"
}
}
}

Back off until reset_at, then retry. Use exponential backoff with jitter if you are processing a queue — synchronizing every worker on reset_at just hits the next window’s limit.

Limits are enforced as a fixed-window count, not a token bucket. A burst that fits inside one window passes; long-term sustained throughput is bounded by the window’s limit. The IP limit and most per-key limits use a 60-second window; dsar:create uses 24 hours.

If you consistently see rate_limit_exceeded and adding backoff is not enough, contact support. Custom tiers are sized to your traffic shape. See the API error codes.

The flat classes need a different fix, because a tier change will not move them:

  • For sessions:read_report, fetch a verification’s report once and keep it in your own storage under your own retention policy, rather than re-fetching it per page view.
  • For sessions:read_image, read idv.available_images on GET /v1/sessions/{id} first and request only the images it lists. That removes the wasted request per missing image, which is the usual cause of hitting this class. See Evidence images.

If a legitimate workload still needs more headroom on either, contact support with the request_id from one 429 and describe the traffic shape; both caps exist to protect the rendering and evidence paths, so raising one is a review rather than a self-serve change.