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

# Errors & limits

> PyAI error envelopes (OpenAI-style and RFC 7807 problem+json), stable error codes, rate limits, retry guidance, and Idempotency-Key semantics.

## Error shapes

PyAI returns two error shapes depending on which layer produced the error. Branch
on the **stable code**, never the human message.

### Data plane (gateway), OpenAI-compatible

Auth, scope, rate-limit, and billing errors use the OpenAI envelope:

```json theme={null}
{
  "error": {
    "message": "Rate limit exceeded.",
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "param": null
  }
}
```

### Control plane, RFC 7807 problem+json

Request-validation and resource errors (e.g. `400`, `404`, `409`) use
`application/problem+json`. The stable code is the **last path segment of
`type`**:

```json theme={null}
{
  "type": "https://api.pyai.com/problems/idempotency_conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "Idempotency-Key was reused with a different request body.",
  "request_id": "req_..."
}
```

## Error code reference

| HTTP | Code                         | Meaning                                                                   | Retry?                   |
| ---- | ---------------------------- | ------------------------------------------------------------------------- | ------------------------ |
| 401  | `unauthorized`               | Missing/invalid/revoked key                                               | No                       |
| 403  | `forbidden`                  | Key lacks the required scope                                              | No                       |
| 403  | `origin_not_allowed`         | Publishable-token origin not allow-listed                                 | No                       |
| 400  | `invalid_agent_id`           | `agent_id` on a realtime/Omni URL is malformed (control chars / too long) | No, fix the id           |
| 402  | `credit_exhausted`           | Org out of prepaid credit                                                 | No, add credit           |
| 402  | `key_budget_exceeded`        | Per-key monthly cap reached                                               | No, raise budget         |
| 402  | `insufficient_quota`         | Plan quota exhausted                                                      | No, upgrade              |
| 409  | `idempotency_conflict`       | `Idempotency-Key` reused with a different body                            | No, use a new key        |
| 429  | `rate_limit_exceeded`        | Per-key/IP rate limit                                                     | Yes, honor `Retry-After` |
| 429  | `concurrency_limit_exceeded` | Too many concurrent realtime sessions                                     | Yes, wait + retry        |
| 429  | `daily_cap_exceeded`         | Sandbox/publishable daily cap                                             | Yes, after reset         |

## Rate limits

Every key has a per-second rate limit (with burst) and a cap on concurrent
realtime sessions, set by your plan. Exceeding either returns `429` with a
`Retry-After` header (seconds to wait). Back off and retry; the official SDKs do
this automatically.

## Idempotency

`POST /v1/transcription/jobs` accepts an `Idempotency-Key` header so a retried
request can't create a duplicate job:

* **Same key + same body** → the original response is replayed (no new job).
* **Same key + different body** → `409 idempotency_conflict`.

<Tip>
  Send a fresh idempotency key (e.g. a UUID) per logical operation, and reuse it
  when retrying that exact operation after a network blip.
</Tip>

## Pagination

List endpoints are cursor-paginated, newest first. Pass `limit` (1-100, default
20\) and the previous page's `next_cursor` as `cursor`. `next_cursor` is `null`
on the last page.
