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 stream metadata — status created.
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:
| Status | Meaning |
|---|---|
created |
Created and started in the single POST /v1/sessions call; ready for the WebSocket connect |
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 carries everything the frontend needs:
session_id— save it; every session-scoped call uses it.stream.ws_url— the ready-to-use WebSocket URL with the embedded short-lived token.stream.token_expires_at— when the token expires.
{
"success": true,
"data": {
"session_id": "session:8f3c1a2b-...",
"status": "created",
"created_at": 1718352000000,
"stream": {
"transport": "websocket_fmp4",
"available_transports": ["websocket_fmp4"],
"ws_url": "wss://<orvyn-public-api-host>/v1/sessions/session:.../ws?token=eyJ...",
"token_expires_at": 1718352300000
}
},
"timings": { "latency_ms": 9 }
}
Checking status
GET /v1/sessions/{session_id} returns the authoritative state and timings:
curl "https://<orvyn-public-api-host>/v1/sessions/$SESSION_ID" \
-H "Authorization: Bearer $AVTR_API_KEY"
{
"success": true,
"data": {
"session_id": "session:8f3c...",
"status": "active",
"created_at": 1718352000000,
"started_at": 1718352001000,
"ended_at": null,
"stream": { "transport": "websocket_fmp4", "ws_url": "...", "status_url": "..." }
},
"timings": { "latency_ms": 45 }
}
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"
{
"success": true,
"data": {
"session_id": "session:8f3c...",
"status": "ended",
"end_ok": true
},
"timings": { "latency_ms": 1820 }
}
On graceful cleanup the response carries status: "ended". If backend cleanup
timed out or partially failed, it carries status: "ended_with_warnings" with
a warning field describing what happened — the session is still terminal.
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 not_found— there is no way to distinguish them, and no information leaks. - Ownership is set once at creation and never changes.
Duplicate and racing calls
The lifecycle is hardened against races:
- A start already in progress makes concurrent lifecycle calls return
409 session_startinguntil it resolves. - Ending a session while its start is still in flight cancels the start
cleanly — the start returns
409 session_cancelledand the session stays ended. There is no resurrection. - Cleanup calls (end) are exempt from rate limits — you can always end a session even if you have exhausted other limits.
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"
Connect the returned ws_url 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 |
|---|---|---|
not_found |
404 | Session doesn’t exist or belongs to another tenant (identical response either way) |
invalid_session_id |
400 | Session id is malformed |
session_conflict |
409 | Session in a conflicting state for the requested operation |
session_starting |
409 | A start is already in progress |
session_cancelled |
409 | The start was cancelled by an end call |
The generated reference for each operation is authoritative — see
POST /v1/sessions,
GET /v1/sessions/{session_id},
and POST /v1/sessions/{session_id}/end.