---
title: Errors, limits & security
description: The error envelope, retry guidance, rate limits, 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 envelope

All errors use the same shape, with the HTTP status preserved:

```json
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Session not found."
  },
  "timings": { "latency_ms": 123 }
}
```

Match on `error.code`, not on the `message` — codes are contract, messages are
prose.

## Core error catalog

The codes you will actually meet in the canonical flow:

| Code | Status | Meaning | What to do |
|---|---|---|---|
| `missing_api_key` | 401 | No `Authorization` header | Send `Authorization: Bearer <api_key>` |
| `invalid_api_key` | 403 | Key unrecognized or revoked | Check the key; create a new one if revoked |
| `invalid_body` | 400 | Body is not valid JSON | Fix serialization |
| `invalid_session_id` | 400 | Session id malformed | Ids start with `session:` |
| `not_found` | 404 | No route matched, or session absent/not yours | Verify id and key ownership |
| `session_conflict` | 409 | Session state conflicts with the operation | See the [lifecycle guide](/guides/session-lifecycle#ending-a-session) |
| `rate_limited` | 429 | Too many requests in the window | Wait `Retry-After` seconds, then retry |
| `internal_error` | 500 | Unexpected server error | Retry with backoff; contact support if persistent |
| `modal_start_failed` | 502 | Rendering backend rejected the start | Retry with a new session |
| `modal_unreachable` | 500 | Rendering backend unreachable | Retry after a brief wait |
| `api_auth_not_configured` | 503 | Server has no keys configured | Platform-team action required |

The full per-endpoint response sets — including the 4xx/5xx entries for each of
the six operations — are rendered on their generated pages in the
[API reference](/reference).

## Error taxonomy on the media WebSocket

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

- **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:** `429` (after `Retry-After`), `500`, `502`, `503`
  transport-class errors.
- **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

All `/v1/` routes carry per-tenant abuse-control limits. They are safety nets,
not billing quotas — nothing is counted or billed.

| Category | Limit (per 60 s window) |
|---|---|
| Session create (`POST /v1/sessions`) | 20 |
| Read (GET routes) | 120 |
| Cleanup (end calls) | **Exempt** |

Limits are enforced per tenant and per edge location, with a 60-second window.
When exceeded, requests return `429 rate_limited` with a `Retry-After` header
carrying the remaining window:

```json
{
  "success": false,
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded for category \"session_create\". Retry after 60 seconds."
  },
  "timings": { "latency_ms": 2 }
}
```

:::tip[Cleanup is always allowed]
Ending a session is exempt from rate limiting by design — you can never be
locked out of releasing GPU resources.
:::

## Correlation ids for support

Every `/v1/` response carries an `X-Request-Id` header (also in the `timings`
object of JSON responses). Supply your own on request 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 |
| WebSocket token (`?token=` in `ws_url`) | 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 Orvyn `client_token`-backed `ws_url` and stream
metadata. If you find yourself forwarding a provider credential to the
frontend, stop — that credential belongs to the backend-to-Orvyn call only.
:::

### Treat ws_url as a credential

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

- **Do** open the connection with the URL as-is (no extra headers).
- **Do** use [client-token refresh](/guides/authentication#token-lifecycle) for
  reconnects.
- **Do not** log it, 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.
