Authentication
API keys for the control plane, short-lived tokens for the media plane — what to use where, and how to handle both safely.
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 |
| Client token | The media WebSocket (?token= query parameter) |
Short-lived, 5 minutes |
API key authentication
All /v1/ routes accept your API key via either header:
Authorization: Bearer <api_key>x-api-key: <api_key>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.
Auth errors
Every error body is the flat AVTR ErrorResponse:
{
"code": "AVTR_AUTH_MISSING_CREDENTIALS",
"message": "Missing API credentials.",
"request_id": "3f2a9c1e-...",
"doc_url": "https://<orvyn-public-api-host>/docs/errors"
}
Auth error codes, per the binding contract:
| Status | code |
Meaning |
|---|---|---|
| 401 | AVTR_AUTH_MISSING_CREDENTIALS |
No credential provided |
| 401 | AVTR_AUTH_INVALID_KEY |
Credential invalid, unrecognized, revoked — or (Clerk) rejected by the authorized-parties (azp) allowlist |
| 401 | AVTR_AUTH_EXPIRED_TOKEN |
Bearer token expired |
| 403 | AVTR_AUTH_FORBIDDEN |
Credential recognized but not permitted |
| 403 | AVTR_AUTH_MISSING_SCOPE |
Scope missing for this endpoint |
| 403 | 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 |
See errors, limits & security for the full
catalog and the retry_after rules.
Client token authentication
The media WebSocket (GET /v1/sessions/{session_id}/ws) is not
API-key authenticated. It uses a short-lived HMAC-signed token passed as the
?token= query parameter:
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 | HMAC-bound to one session 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 token (and any URL you assemble with it) must be handled like a credential.
Token lifecycle
You receive the first token — client_token — in the POST /v1/sessions
response, delivered exactly once (expires_at carries its epoch-ms expiry).
For reconnects after the 5-minute expiry, mint a fresh one:
curl -X POST \
"https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID/client-token" \
-H "Authorization: Bearer $AVTR_API_KEY"
The response is a bare ClientTokenResponse:
{
"client_token": "<fresh-hmac-client-token>",
"expires_at": 1718352600000,
"transport": "websocket_fmp4",
"stream_metadata": {
"session_token": "<purpose-scoped-media-token>",
"sfu_endpoint": null,
"ice_servers": []
}
}
| Field | Meaning |
|---|---|
client_token |
Freshly-minted short-lived (5-minute) client token |
expires_at |
Unix epoch milliseconds when the new token expires |
transport |
Transport for this session: auto, webrtc_sfu, or websocket_fmp4 |
stream_metadata.session_token |
Purpose-scoped media-plane token — an alternative ?token= value |
stream_metadata.sfu_endpoint |
SFU publisher endpoint; null on the websocket_fmp4 transport |
stream_metadata.ice_servers |
ICE servers for WebRTC negotiation; may be empty for websocket_fmp4 |
Build the WebSocket URL as
wss://<orvyn-public-api-host>/v1/sessions/{session_id}/ws?token=<client_token>
with the fresh token.
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 client_token (or an assembled media URL) in logs/analytics |
Build the URL, open the connection, then discard both |
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. Error responses echo the same id as
request_id in the error body (ErrorResponse.request_id); success bodies
are bare schema objects and carry no such field.
Response shape contract
Successful responses are bare schema objects — the exact shape documented
for each endpoint (SessionResponse, SessionStatusResponse,
EndSessionResponse, ClientTokenResponse, …). There is no envelope:
{
"session_id": "session:8f3c...",
"status": "active"
}
Errors keep the flat AVTR shape — {code, message, request_id, doc_url} plus
retry_after on retryable codes only — see
errors, limits & security.