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.
How rate limits work
Section titled “How rate limits work”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,customincluded. 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 exampleGET /v1/sessions,GET /v1/sessions/{id},GET /v1/applicants,GET /v1/workflow-templatesandGET /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.
Rate-limit headers
Section titled “Rate-limit headers”Every authenticated response carries the per-key limit state:
X-RateLimit-Limit: 1000X-RateLimit-Remaining: 873X-RateLimit-Reset: 1717000060X-RateLimit-Window: 60X-RateLimit-Limit— the request budget for the current window.X-RateLimit-Remaining— how many requests are left before429.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.
When you hit the limit
Section titled “When you hit the limit”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.
Burst vs sustained rate
Section titled “Burst vs sustained rate”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.
Asking for a higher limit
Section titled “Asking for a higher limit”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, readidv.available_imagesonGET /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.