VVEREID Docs
concepts

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

BucketEndpoint patternDefault limit
social_readGET /v1/social/*, GET /v1/profiles/*, GET /v1/network/*200 req/min per key
social_writenon-GET on the patterns above60 req/min per key
auth_tokenPOST /v1/oidc/token, POST /v1/oidc/par, POST /v1/oauth2/token300 req/min per client
auth_userinfoGET /v1/oidc/userinfo600 req/min per client
verify_createPOST /v1/verify/sessions60 req/min per key
verify_readGET /v1/verify/*600 req/min per key
verify_adminPOST /v1/verify/sessions/*/decisions30 req/min per key
webhooks_replayPOST /v1/developer/webhooks/*/replay20 req/min per key
defaulteverything else120 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:     27

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

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 /healthz is unmetered.
  • The OpenAPI spec at /openapi.yaml is 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.