---
title: Authentication
description: API keys for the control plane, short-lived WebSocket tokens for the media stream — what to use where, and how to handle both safely.
sidebar:
  label: Authentication
  order: 1
---

Every Orvyn API surface is authenticated, with **two different credential
types** for two different planes:

| Credential | Where it is used | Lifetime |
|---|---|---|
| **API key** | All six `/v1/` REST endpoints (headers) | Long-lived, admin-managed |
| **WebSocket token** | The media WebSocket (`?token=` in `ws_url`) | Short-lived, 5 minutes |

## API key authentication

All `/v1/` routes accept your API key via either header:

<CodeGroup>

```http Authorization
Authorization: Bearer <api_key>
```

```http x-api-key
x-api-key: <api_key>
```

</CodeGroup>

### Where keys come from

API keys are created and revoked through the admin surface of the API itself:

- `POST /v1/api-keys` — create a key. The plaintext is shown **exactly once** at
  creation; Orvyn stores only a SHA-256 hash.
- `DELETE /v1/api-keys/{key_id}` — revoke a key. Revocation takes effect
  immediately; keys do not expire on their own.

:::danger[Never expose API keys]
API keys are long-lived credentials.

- Keep them in backend secrets or environment bindings — never in frontend
  code, source, or checked-in config.
- Never send them in URL query strings.
- If a key leaks, revoke it immediately with
  `DELETE /v1/api-keys/{key_id}`.
:::

### Auth errors

| Status | `error.code` | Meaning |
|---|---|---|
| 401 | `missing_api_key` | No key provided |
| 403 | `invalid_api_key` | Key not recognized or revoked |
| 503 | `api_auth_not_configured` | Server has no keys configured |

## WebSocket token authentication

The media WebSocket (`GET /v1/sessions/{session_id}/ws`) is **not**
API-key authenticated. It uses a short-lived HMAC-signed token embedded in the
connection URL:

```
wss://<orvyn-public-api-host>/v1/sessions/{session_id}/ws?token=...
```

### Token properties

| Property | Value |
|---|---|
| Signing | HMAC-SHA256 |
| TTL | 5 minutes (300 seconds) |
| Payload | Session id, tenant id, purpose — never the raw API key |
| Binding | Bound to one session id and one tenant |

The token is **the authorization**: anyone holding a valid token can connect to
that session's WebSocket. This is by design, but it means the URL must be
handled like a credential.

### Token lifecycle

You receive the first token in the `POST /v1/sessions` response (`stream.ws_url`
includes it). For reconnects after the 5-minute expiry, mint a fresh one:

```bash
curl -X POST \
  "https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID/client-token" \
  -H "Authorization: Bearer $AVTR_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "ws_url": "wss://<orvyn-public-api-host>/v1/sessions/session:.../ws?token=eyJ...",
    "token_expires_at": 1718352600000
  },
  "timings": { "latency_ms": 3 }
}
```

:::note[Refreshing does not revoke]
Previously issued tokens stay valid until their own expiry. There is no
per-token revocation — the 5-minute TTL is the revocation window. If the token
secret itself is compromised, the platform rotates it, which invalidates all
outstanding tokens at once.
:::

## What to never put in a URL

| Never in a URL | Use instead |
|---|---|
| API keys (`Authorization` values) | Headers on REST calls |
| Long-lived provider credentials | `provider_config` in `POST /v1/sessions`, minted per session by your backend |
| The full `ws_url` in logs/analytics | Open the connection, then discard the URL |

## Request correlation

Every `/v1/` response includes an `X-Request-Id` header for tracing. You may
supply your own (1–128 characters of `[a-zA-Z0-9_-]`); it is echoed back when
valid, otherwise a fresh id is generated. The id also appears in the `timings`
object of JSON responses.

## Request shape contract

Every response uses one envelope:

```json
{
  "success": true,
  "data": {},
  "timings": { "latency_ms": 123 }
}
```

Errors keep the same shape with `"success": false` and an `error` object
carrying a stable `code` — see
[errors, limits & security](/guides/errors-limits-security).
