---
title: Errors, limits & security
description: The flat error shape, the AVTR_* error catalog, rate-limit headers, and the security rules every integration must follow.
sidebar:
  label: Errors, limits & security
  order: 4
---

Every failure has a stable, machine-readable identity — and every credential
has a strict handling rule. This page is the operational summary; the generated
[API reference](/reference) is authoritative for per-endpoint response codes.

## The error shape

Every 4xx/5xx response uses the flat AVTR error shape — no envelope, no nested
`error` object:

```json
{
  "code": "AVTR_SESSION_NOT_FOUND",
  "message": "Session not found.",
  "request_id": "3f2a9c1e-...",
  "doc_url": "https://<orvyn-public-api-host>/docs/errors"
}
```

| Field | Present | Meaning |
|---|---|---|
| `code` | always | Stable `AVTR_*` identifier — match on this, never on `message` |
| `message` | always | Human-readable, provider-agnostic prose |
| `request_id` | always | Correlation UUID; echoes the `X-Request-Id` response header |
| `doc_url` | always | Absolute HTTPS URL documenting this code |
| `retry_after` | retryable codes only | Seconds to wait (1–3600) before retrying |

`retry_after` is **required** on retryable codes and **forbidden** on
non-retryable codes — its presence tells you which kind you got. Honor the
value and apply jitter when retrying.

## Error catalog

All codes carry the `AVTR_` prefix. The taxonomy has two classes:

### Retryable codes (`retry_after` required)

| Code | Typical situation | What to do |
|---|---|---|
| `AVTR_PROVIDER_DOWNSTREAM_FAILED` | Upstream provider returned an error or a downstream step failed | Retry after `retry_after` |
| `AVTR_PROVIDER_TIMEOUT` | Provider call timed out | Retry after `retry_after` |
| `AVTR_PROVIDER_UNAVAILABLE` | Provider unavailable, session state unavailable, or capacity exhausted | Retry after `retry_after` |
| `AVTR_RATE_LIMIT_CONCURRENT_SESSIONS` | Concurrent-session limit hit | Wait, then retry |
| `AVTR_RATE_LIMIT_EXCEEDED` | Too many requests in the window | Honor `Retry-After`, then retry |
| `AVTR_RATE_LIMIT_PROVIDER_QUOTA` | Provider quota exhausted | Wait, then retry |
| `AVTR_RENDER_CAPACITY` | Rendering capacity exhausted | Retry after `retry_after` |
| `AVTR_RENDER_TIMEOUT` | Provider or render timeout | Retry after `retry_after` |
| `AVTR_SESSION_STATE_UNAVAILABLE` | Session state temporarily unavailable | Retry after `retry_after` |
| `AVTR_SFU_CONNECTION_FAILED` | SFU connection failed | Retry after `retry_after` |
| `AVTR_SFU_NEGOTIATION_FAILED` | SFU negotiation failed | Retry after `retry_after` |
| `AVTR_SFU_ROOM_FULL` | SFU room at capacity | Retry after `retry_after` |

### Non-retryable codes (`retry_after` forbidden)

| Code | Typical situation | What to do |
|---|---|---|
| `AVTR_AUTH_EXPIRED_TOKEN` | Bearer credential expired | Obtain a fresh credential |
| `AVTR_AUTH_FORBIDDEN` | Credential recognized but not permitted — API keys: key lacks the caller's required permission; Clerk: valid token rejected by the authorized-parties (`azp`) allowlist | Use a key with the required permission / present the token to an allowed origin |
| `AVTR_AUTH_CLERK_ORG_UNMAPPED` | Valid Clerk credential but the Clerk org is not mapped to an AVTR org | Map the org or contact support |
| `AVTR_AUTH_INVALID_KEY` | Key unrecognized or revoked | Check the key; create a new one if revoked |
| `AVTR_AUTH_MISSING_CREDENTIALS` | No key provided | Send `Authorization: Bearer <api_key>` or `x-api-key` |
| `AVTR_AUTH_MISSING_SCOPE` | Key lacks the route's scope | Re-create the key with the needed scope |
| `AVTR_PROVIDER_MISCONFIGURED` | `provider_type` / `provider_config` mismatch or unknown provider | Fix the request body |
| `AVTR_RENDER_FAILED` | Rendering failed terminally | Create a new session |
| `AVTR_RENDER_MISCONFIGURED` | Render configuration invalid | Fix the session configuration |
| `AVTR_SESSION_CANCELLED` | Session was cancelled | Create a new session |
| `AVTR_SESSION_CONFLICT` | `/end` posted against `created` / `ending` / `failed` | Treat as cleanup-success or re-read status |
| `AVTR_SESSION_ENDED` | `/end` re-posted against `ended` / `ended_with_warnings` | Treat as success in cleanup logic |
| `AVTR_SESSION_NOT_FOUND` | Session absent — or belongs to another tenant (identical response either way) | Verify the id and key ownership |
| `AVTR_SFU_NOT_CONFIGURED` | SFU requested but not available for the account | Use another transport |

The per-endpoint status codes are documented on each generated reference page;
the catalog above tells you which class a code belongs to and how to react.

## Error taxonomy on the media WebSocket

The media WebSocket has its own error surface
(`{ "type": "error", "code": "...", "fatal": false }` messages on the socket) —
separate from the REST taxonomy above:

- **Fatal** (socket closes): only `invalid_session` — recover by creating a
  new session.
- **Lifecycle** (socket stays open): e.g. `avatar_not_found`,
  `session_already_active`.
- **Pipeline, per-turn** (session survives): `stt_failed`, `llm_failed`,
  `tts_failed`, `avatar_render_failed` — the avatar may miss the turn, but the
  next one can succeed.

Details in the [WebSocket guide](/guides/websocket-fmp4#errors-on-the-socket).

## Retries

- **Retry with backoff:** retryable codes — the response tells you so by
  carrying `retry_after` (and `429` responses carry the `Retry-After` header).
  Honor the value, add jitter.
- **Do not blind-retry:** `409` conflicts mean the session state moved on —
  re-read the session status instead.
- **Idempotent-by-nature:** status reads are always safe to retry.
- **End is deliberately not idempotent:** a `409` on re-end means the session
  is already terminal — treat it as success in cleanup logic.

## Rate limits

Every `/v1/` response carries rate-limit headers:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset` | Unix epoch seconds at which the window resets |

When a request exceeds the limit it returns `429` with a `Retry-After` header —
seconds until the rate-limit window resets — and a flat error body whose
`code` is one of the rate-limit codes above (`AVTR_RATE_LIMIT_EXCEEDED`,
`AVTR_RATE_LIMIT_CONCURRENT_SESSIONS`, or `AVTR_RATE_LIMIT_PROVIDER_QUOTA`),
with the matching `retry_after` value in the body:

```json
{
  "code": "AVTR_RATE_LIMIT_EXCEEDED",
  "message": "Rate limit exceeded. Retry later.",
  "request_id": "3f2a9c1e-...",
  "doc_url": "https://<orvyn-public-api-host>/docs/errors",
  "retry_after": 30
}
```

:::tip[Watch the headers, not the numbers]
Concrete window sizes and quotas are deployment-specific — read
`X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` at runtime
instead of hardcoding limits.
:::

## Correlation ids for support

Every `/v1/` response carries an `X-Request-Id` header, and error responses
echo the same value as `request_id` in the error body
(`ErrorResponse.request_id`). Success bodies are bare schema objects and carry
no such field.
Supply your own on request (1–128 characters of `[a-zA-Z0-9_-]`) to correlate
across your systems, and include it when contacting support — it links your
request to the platform's server-side traces end to end.

## Security rules

### Credential boundaries

| Credential | Lives where | Never |
|---|---|---|
| API key | Backend secret / env binding | In frontend code, URLs, source, or logs |
| Provider tokens (`client_secret`, `access_token`, …) | Your backend, minted per session, passed in `provider_config` | Sent to the browser, persisted, or logged |
| Client token (`?token=` on the media WebSocket) | The browser, for the connection only | Logged, screenshotted, or stored in analytics |

### Provider credentials are memory-only

Provider credentials passed in `POST /v1/sessions` are held **in memory for the
session lifetime only**, then zeroed. They are never written to any database,
cache, file, or log. Your backend must mint a fresh provider token for every
session — treat provider tokens as short-lived by design.

:::danger[Never send provider credentials to the frontend]
The browser receives only the session's `client_token` /
`stream_metadata.session_token` and the stream metadata needed to connect. If
you find yourself forwarding a provider credential to the frontend, stop — that
credential belongs to the backend-to-Orvyn call only.
:::

### Treat the client token as a credential

The `?token=` query parameter authorizes the media WebSocket on its own:

- **Do** build the WebSocket URL with the token and open the connection
  as-is (no extra headers).
- **Do** use [client-token refresh](/guides/authentication#token-lifecycle) for
  reconnects.
- **Do not** log the token or the assembled URL, put it in error reports, or
  persist it in browser storage.

### Key hygiene

- Create keys via `POST /v1/api-keys`; the plaintext is shown exactly once.
- Store only backend-side; rotate by revoking (`DELETE /v1/api-keys/{key_id}`)
  and re-creating.
- A revoked key stops working immediately.

## Reporting a security concern

Found a potential vulnerability? Do not open a public issue. Contact the
platform team directly through your account channel.
