> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate Limiting

> Didit API rate limits, X-RateLimit headers, Retry-After backoff, per-endpoint scopes, and 429 handling. Plan production traffic with confidence.

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:

| Scope           | HTTP method(s)            | Limit     | Window |
| --------------- | ------------------------- | --------- | ------ |
| `generic-get`   | `GET`                     | 600 / min | 60 s   |
| `generic-write` | `POST`, `PATCH`, `DELETE` | 300 / min | 60 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:

| Scope                  | Endpoint(s)                                                                | Limit     | Source constant                 |
| ---------------------- | -------------------------------------------------------------------------- | --------- | ------------------------------- |
| `session-v2-create`    | `POST /v2/session/`, `POST /v3/session/`                                   | 600 / min | `SESSION_CREATE_RATE_LIMIT`     |
| `session-decision`     | `GET /v1/session/<id>/decision/`, `GET /v2/session/<id>/decision/`         | 100 / min | `SESSION_DECISION_RATE_LIMIT`   |
| `session-generate-pdf` | `GET /v1/session/<id>/generate-pdf/`, `GET /v3/session/<id>/generate-pdf/` | 50 / min  | `SESSION_PDF_RATE_LIMIT`        |
| `session-add-images`   | `POST`/`PATCH /session/<id>/add-images/`                                   | 10 / min  | `SESSION_ADD_IMAGES_RATE_LIMIT` |
| `session-update-kyc`   | `POST`/`PATCH /session/<id>/update-data/`                                  | 10 / min  | `SESSION_UPDATE_KYC_RATE_LIMIT` |
| `session-update-poa`   | `POST`/`PATCH /session/<id>/update-poa-data/`                              | 10 / min  | `SESSION_UPDATE_POA_RATE_LIMIT` |

<Note>
  `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.
</Note>

<Warning>
  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.
</Warning>

## 429 response shape

When you exceed any scope you get an HTTP `429` with this body and headers:

```http theme={null}
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."
}
```

| Header                  | Meaning                                                                                             |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `X-RateLimit-Limit`     | Max requests permitted in the current window for the scope that tripped.                            |
| `X-RateLimit-Remaining` | Remaining requests before the window resets. Clamped to `0` once you cross the limit.               |
| `X-RateLimit-Reset`     | Unix epoch seconds when the counter resets to zero.                                                 |
| `Retry-After`           | Seconds 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](/integration/programmatic-registration#managing-applications).
* **Cache decision reads.** The `/decision/` endpoints are not designed for tight polling loops; if you need real-time updates, use [Webhooks](/integration/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

```javascript theme={null}
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");
}
```

```python theme={null}
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](mailto:support@didit.me) with your `application` UUID and the expected sustained and burst rates. Overrides are configured per application.
