Rate limits
Per-endpoint rate buckets, what 429 looks like on the wire, and a recommended back-off strategy.
Last updated 2026-05-20
VEREID applies rate limits at the API key scope. Limits are sliding-window over 60 seconds and are independent across product surfaces, so heavy use of the social API does not starve verification calls.
Buckets
| Bucket | Endpoint pattern | Default limit |
|---|---|---|
social_read | GET /v1/social/*, GET /v1/profiles/*, GET /v1/network/* | 200 req/min per key |
social_write | non-GET on the patterns above | 60 req/min per key |
auth_token | POST /v1/oidc/token, POST /v1/oidc/par, POST /v1/oauth2/token | 300 req/min per client |
auth_userinfo | GET /v1/oidc/userinfo | 600 req/min per client |
verify_create | POST /v1/verify/sessions | 60 req/min per key |
verify_read | GET /v1/verify/* | 600 req/min per key |
verify_admin | POST /v1/verify/sessions/*/decisions | 30 req/min per key |
webhooks_replay | POST /v1/developer/webhooks/*/replay | 20 req/min per key |
default | everything else | 120 req/min per key |
Enterprise customers can raise limits per bucket via support. Test keys carry the same limits as live keys so the failure modes are identical.
Wire format
Every response carries the three standard headers:
ratelimit-limit: 200
ratelimit-remaining: 184
ratelimit-reset: 27ratelimit-reset is seconds until the window resets, not a Unix timestamp. We follow the IETF rate-limit-headers draft.
When you exhaust a bucket, the response is HTTP 429 with a retry-after header and an RFC 9457 problem body:
{
"type": "https://docs.vereid.com/errors/rate_limited",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Bucket 'verify_create' exhausted; retry after 12 seconds.",
"bucket": "verify_create",
"retry_after": 12
}The header is the authoritative value to read — never parse the body to obtain retry-after.
Recommended back-off
A simple full-jitter exponential back-off bounded by the retry-after value handles every legitimate 429:
async function withRetry(fn, max = 5) {
for (let attempt = 0; attempt < max; attempt++) {
const res = await fn();
if (res.status !== 429) return res;
const retryAfter = Number(res.headers.get("retry-after") ?? 1);
const jitter = Math.random() * retryAfter;
await new Promise((r) => setTimeout(r, (retryAfter + jitter) * 1000));
}
throw new Error("rate-limit retries exhausted");
}import time, random, httpx
def with_retry(call, max_attempts: int = 5):
for _ in range(max_attempts):
r = call()
if r.status_code != 429:
return r
ra = float(r.headers.get("retry-after", "1"))
time.sleep(ra + random.random() * ra)
raise RuntimeError("rate-limit retries exhausted")Concurrency vs. throughput
Bucket counts are per-window, not per-connection. Opening more sockets does not raise your effective throughput. If you need to burst, batch your requests or request a quota increase rather than parallelising.
For bulk imports (e.g. migrating a Persona inquiry history), use the POST /v1/admin/bulk-import endpoint which has its own bucket of 5 req/min but accepts up to 10,000 records per call.
SDK behaviour
All official SDKs implement the algorithm above automatically and surface the final 429 only if max_attempts is exhausted. You can override the default via Vereid({ retry: { maxAttempts: 8 } }).
What does not count
GET /healthzis unmetered.- The OpenAPI spec at
/openapi.yamlis served from CloudFront with no key required. - Webhooks delivered to you do not consume any of your buckets — they only consume the global outbound budget on our side.
Test-mode behaviour
Test keys honour the same limits but the buckets are namespaced per-test-tenant, so two developers in the same org cannot starve each other. Test-mode 429s also surface a vereid-debug-bucket-state header showing the current window so you can write deterministic tests against the limiter.