Session lifecycle
What happens when you create a session — states, ownership, ending semantics, and how to recover from conflicts.
A session is one live avatar interaction. This guide walks its life from create-and-start through ending, and the rules that protect it: per-call states, tenant ownership, and non-idempotent cleanup.
Life of a session
Create + start (one call)
POST /v1/sessions creates the session, starts rendering, and returns the
client_token with stream metadata.
Connect + interact
The browser connects the media WebSocket and exchanges turns with the avatar.
End
Your backend posts /end — resources are released and the session is terminal.
Session states
The status field tracks where a session is. The lifecycle enum has exactly
seven values:
| Status | Meaning |
|---|---|
created |
Created and started in the single POST /v1/sessions call; ready for the WebSocket connect |
activating |
Start is in flight — provider/render resources are being provisioned |
active |
Rendering and interacting — media is flowing |
ending |
An end call is in flight |
ended |
Fully torn down (terminal) |
ended_with_warnings |
Torn down, but cleanup reported a warning field (terminal) |
failed |
Startup error — the session is dead (terminal) |
Create-and-start in one call
The canonical create is a single atomic call. There is no separate start step in the canonical V0 flow:
curl -X POST "https://<orvyn-public-api-host>/v1/sessions" \
-H "Authorization: Bearer $AVTR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider_type": "openai_realtime",
"provider_config": {
"openai_realtime": { "client_secret": "<ephemeral-client-secret>" }
}
}'
The response is a bare SessionResponse carrying everything the frontend
needs:
session_id— save it; every session-scoped call uses it.client_token— the short-lived media token, delivered exactly once (it never appears in aGETresponse).expires_at— epoch ms whenclient_tokenexpires.transport+stream_metadata— the media-plane connection metadata.
{
"session_id": "session:8f3c1a2b-...",
"client_token": "<hmac-client-token>",
"transport": "websocket_fmp4",
"expires_at": 1718352300000,
"status": "created",
"stream_metadata": {
"session_token": "<purpose-scoped-media-token>",
"sfu_endpoint": null,
"ice_servers": []
}
}
Build the media WebSocket URL as
wss://<orvyn-public-api-host>/v1/sessions/{session_id}/ws?token=<client_token>.
Checking status
GET /v1/sessions/{session_id} returns the authoritative state as a bare
SessionStatusResponse. It never includes client_token:
curl "https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID" \
-H "Authorization: Bearer $AVTR_API_KEY"
{
"session_id": "session:8f3c...",
"transport": "websocket_fmp4",
"status": "active",
"provider_type": "openai_realtime",
"created_at": 1718352000000,
"started_at": 1718352001000,
"ended_at": 1718352182000
}
| Field | Always present? | Meaning |
|---|---|---|
session_id |
yes | session:-prefixed id |
transport |
yes | auto, webrtc_sfu, or websocket_fmp4 |
status |
yes | One of the seven lifecycle states |
provider_type |
yes | Provider selected at creation (e.g. openai_realtime) |
created_at, started_at, ended_at |
when set | Unix epoch milliseconds |
Ending a session
End sessions from your backend:
curl -X POST "https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID/end" \
-H "Authorization: Bearer $AVTR_API_KEY"
A successful end returns a bare EndSessionResponse:
{
"session_id": "session:8f3c...",
"status": "ended",
"ended_at": 1718352182000
}
| Field | Always present? | Meaning |
|---|---|---|
session_id |
yes | session:-prefixed id |
status |
yes | ended or ended_with_warnings |
ended_at |
yes | Unix epoch ms when the session ended |
warning |
only on ended_with_warnings |
What timed out or partially failed during cleanup |
Append ?force=true to skip the graceful disconnect handshake and terminate
backend resources immediately.
Session ownership
Every session is bound to the tenant that created it:
- All session-scoped calls verify that the caller’s tenant matches the stored owner.
- A session that doesn’t exist or belongs to another tenant returns the
identical
404 AVTR_SESSION_NOT_FOUND— there is no way to distinguish them, and no information leaks. - Ownership is set once at creation and never changes.
Recovering a dropped connection
If the browser disconnects (network drop, sleep), don’t create a new session — mint a fresh token for the existing one:
curl -X POST \
"https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID/client-token" \
-H "Authorization: Bearer $AVTR_API_KEY"
Build the WebSocket URL from the fresh client_token (or
stream_metadata.session_token) and continue. The 5-minute token TTL is the
only thing that expired — the session itself is unaffected.
Error reference for this guide
| Code | Status | Meaning |
|---|---|---|
AVTR_SESSION_NOT_FOUND |
404 | Session doesn’t exist or belongs to another tenant (identical response either way) |
AVTR_SESSION_ENDED |
409 | /end re-posted against an ended / ended_with_warnings session |
AVTR_SESSION_CONFLICT |
409 | /end posted against a created / ending / failed session |
AVTR_SESSION_STATE_UNAVAILABLE |
503 | Session state temporarily unavailable — retry with retry_after |
The generated reference for each operation is authoritative — see
POST /v1/sessions,
GET /v1/sessions/{session_id},
and POST /v1/sessions/{session_id}/end.