Skip to main content
Didit enforces multiple layers of rate limiting to keep the API stable while still allowing healthy bursts of traffic. Limits are applied per identifier — your x-api-key or, if no API key is sent, your client IP. Each scope keeps an independent counter with a 60-second sliding window.

Identifier and exemptions

The middleware picks a rate-limit identifier in this order in this order:
  1. x-api-key header → key-scoped counter.
  2. X-Forwarded-For first hop → IP-scoped counter.
  3. REMOTE_ADDR → IP-scoped counter.
The following calls bypass rate limiting entirely:
  • The internal DIDIT_WEBAPP_API_KEY (used only by the Didit console).
  • Endpoints protected by the IsValidSession permission (the embedded/SDK session traffic).
  • GET /system/healthcheck (served by HealthMiddleware before this stack).
  • Provider webhooks: Stripe, AML watcher, face verification callbacks, KYC list mutation.

Global limits

Every authenticated request is also bucketed by HTTP method into a generic scope, so any new route inherits a sane default:
ScopeHTTP method(s)LimitWindow
generic-getGET600 / min60 s
generic-writePOST, PATCH, DELETE300 / min60 s
Endpoint-specific limits (next section) apply in addition to the global limit. The first scope to exceed its counter is the one that returns 429.

Endpoint-specific limits

The middleware defines stricter scopes for the highest-impact routes:
ScopeEndpoint(s)LimitSource constant
session-v2-createPOST /v2/session/, POST /v3/session/600 / minSESSION_CREATE_RATE_LIMIT
session-decisionGET /v1/session/<id>/decision/, GET /v2/session/<id>/decision/100 / minSESSION_DECISION_RATE_LIMIT
session-generate-pdfGET /v1/session/<id>/generate-pdf/, GET /v3/session/<id>/generate-pdf/50 / minSESSION_PDF_RATE_LIMIT
session-add-imagesPOST/PATCH /session/<id>/add-images/10 / minSESSION_ADD_IMAGES_RATE_LIMIT
session-update-kycPOST/PATCH /session/<id>/update-data/10 / minSESSION_UPDATE_KYC_RATE_LIMIT
session-update-poaPOST/PATCH /session/<id>/update-poa-data/10 / minSESSION_UPDATE_POA_RATE_LIMIT
GET /v3/session/<id>/decision/ is served by SessionV3RetrieveView and is not in the session-decision scope. It is governed only by the generic-get 600/min ceiling. The 100/min decision throttle applies to the v1 and v2 decision paths.
The codebase also defines FREE_SESSION_RATE_LIMIT = 10 and PAID_SESSION_RATE_LIMIT = 600. These constants are not currently applied to POST /v3/session/ — that endpoint uses the single session-v2-create limit of 600/min for every application. Do not plan capacity around a per-workflow free-tier throttle until it is documented as live.

429 response shape

When you exceed any scope you get an HTTP 429 with this body and headers:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762534870
Retry-After: 37

{
  "detail": "Session creation rate limit exceeded. You can make up to 600 requests per minute."
}
HeaderMeaning
X-RateLimit-LimitMax requests permitted in the current window for the scope that tripped.
X-RateLimit-RemainingRemaining requests before the window resets. Clamped to 0 once you cross the limit.
X-RateLimit-ResetUnix epoch seconds when the counter resets to zero.
Retry-AfterSeconds to wait before the next attempt. Present only on 429 responses, not on every request.
X-RateLimit-* headers are only emitted on the 429 response — successful responses do not include them today. Use the Retry-After value (or X-RateLimit-Reset - now) as your delay before the next attempt.

Client guidance

  • Respect Retry-After as the floor for your backoff. On top of it, layer exponential backoff with jitter (5s → 10s → 20s → 40s ± random) so two clients that hit 429 at the same time do not synchronise.
  • Stop retrying after a small ceiling (e.g. 5 attempts). Surface the error to your caller and alert your team — sustained 429s usually mean a runaway loop, not a transient spike.
  • Use one identifier per workload. Sharing an x-api-key across many backends collapses them into the same counter, which can cause one noisy neighbour to starve the rest. Issue a separate application (and therefore a separate api_key) per environment, brand, or customer — see Programmatic registration.
  • Cache decision reads. The /decision/ endpoints are not designed for tight polling loops; if you need real-time updates, use Webhooks and only call GET /v3/session/<id>/decision/ for back-fill or audit.
  • Generate PDFs offline. PDF rendering is CPU-bound (50/min cap). Trigger it from a queue after the session reaches a terminal status, not on every page view.

Sample 429-aware client

async function postSession(body) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch("https://verification.didit.me/v3/session/", {
      method: "POST",
      headers: {
        "x-api-key": process.env.DIDIT_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (res.status !== 429) return res;

    const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
    const jitter = Math.random() * 1000;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
  }
  throw new Error("Didit: too many 429s, giving up");
}
import os
import random
import time

import httpx

def post_session(body: dict) -> httpx.Response:
    with httpx.Client(timeout=15) as client:
        for _ in range(5):
            resp = client.post(
                "https://verification.didit.me/v3/session/",
                headers={
                    "x-api-key": os.environ["DIDIT_API_KEY"],
                    "Content-Type": "application/json",
                },
                json=body,
            )
            if resp.status_code != 429:
                return resp
            wait_s = float(resp.headers.get("Retry-After", "1")) + random.random()
            time.sleep(wait_s)
    raise RuntimeError("Didit: too many 429s, giving up")

Need more headroom?

If 600 sessions per minute is not enough — for example, a marketing launch or a backfill — email support@didit.me with your application UUID and the expected sustained and burst rates. Overrides are configured per application.