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

# Rate limits

> Tiered limits per endpoint category. Surfaced in headers; degraded gracefully under load.

Kynva rate-limits requests per account. Limits are enforced per **endpoint category**, not as a single global bucket — so a flood of status polls can't starve your renders. Limits scale with your plan tier.

## Categories and free-tier limits

| Category          | Endpoints                         | Free tier | Scales to (Business+) |
| ----------------- | --------------------------------- | --------- | --------------------- |
| **Render**        | `POST /facade/generate`           | 10 / min  | 200+ / min            |
| **Job status**    | `GET /facade/render-jobs/{id}`    | 60 / min  | 600+ / min            |
| **Signed images** | `GET /api/v1/generate/{template}` | 30 / min  | 300+ / min            |
| **Billing**       | `/facade/billing/*`               | 10 / min  | 50+ / min             |
| **Default**       | everything else                   | 60 / min  | 600+ / min            |

<Note>
  Need higher limits? Email [support@kynva.ai](mailto:support@kynva.ai) with your use case. We routinely lift limits for production workloads.
</Note>

## Response headers

Every response includes:

| Header                  | Meaning                                          |
| ----------------------- | ------------------------------------------------ |
| `X-RateLimit-Limit`     | Cap for this category in the current window.     |
| `X-RateLimit-Remaining` | Calls left in the current window.                |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the window resets. |

When you exceed a limit, the response is **429 Too Many Requests** with a `Retry-After` header.

## Hitting the limit

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again later.",
    "details": [
      {
        "message": "Retry after 12 seconds",
        "severity": "error",
        "context": { "retry_after": 12 }
      }
    ]
  }
}
```

## Backoff that works

A robust retry loop:

1. On `429`, read `Retry-After`. Wait at least that long.
2. On `5xx`, exponential backoff: 1s, 2s, 4s, 8s, with ±20% jitter.
3. Cap retries at 5. After that, surface the failure.
4. **Always reuse the same `Idempotency-Key` across retries** — see [idempotency](/concepts/idempotency).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { randomUUID } from "node:crypto";

  async function renderWithBackoff(body: object, attempt = 0): Promise<Response> {
    const key = (globalThis as any).__key ??= randomUUID();

    const res = await fetch("https://api.kynva.ai/api/v1/facade/generate", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.KYNVA_API_KEY!}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    });

    if (res.ok || attempt >= 5) return res;

    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") ?? "1") * 1000;
      await new Promise(r => setTimeout(r, wait));
      return renderWithBackoff(body, attempt + 1);
    }

    if (res.status >= 500) {
      const wait = Math.min(2 ** attempt * 1000, 16_000);
      const jitter = wait * (0.8 + Math.random() * 0.4);
      await new Promise(r => setTimeout(r, jitter));
      return renderWithBackoff(body, attempt + 1);
    }

    return res;
  }
  ```

  ```python Python theme={null}
  import os, time, uuid, random, requests

  def render_with_backoff(body, attempt=0, key=None):
      key = key or str(uuid.uuid4())

      res = requests.post(
          "https://api.kynva.ai/api/v1/facade/generate",
          headers={
              "Authorization": f"Bearer {os.environ['KYNVA_API_KEY']}",
              "Content-Type": "application/json",
              "Idempotency-Key": key,
          },
          json=body,
          timeout=60,
      )

      if res.ok or attempt >= 5:
          return res

      if res.status_code == 429:
          wait = int(res.headers.get("Retry-After", "1"))
          time.sleep(wait)
          return render_with_backoff(body, attempt + 1, key)

      if res.status_code >= 500:
          base = min(2 ** attempt, 16)
          time.sleep(base * (0.8 + random.random() * 0.4))
          return render_with_backoff(body, attempt + 1, key)

      return res
  ```
</CodeGroup>

## Tips for staying under the limit

* **Batch when you can**. Use `POST /facade/render-jobs` with multiple briefs instead of N synchronous renders.
* **Cache aggressively**. GET responses include strong ETags; respect them.
* **Use the async path**. Render jobs don't block on your client connection — fire-and-subscribe-to-webhook is cheaper for both sides.
* **Pre-resolve brands**. List brands once on app boot, not per render.
