---
title: Session lifecycle
description: What happens when you create a session — states, ownership, ending semantics, and how to recover from conflicts.
sidebar:
  label: Session lifecycle
  order: 2
---

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

1. **Create + start (one call)**

    POST /v1/sessions creates the session, starts rendering, and returns the
    `client_token` with stream metadata.

2. **Connect + interact**

    The browser connects the media WebSocket and exchanges turns with the
    avatar.

3. **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:

```bash
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 a `GET` response).
- `expires_at` — epoch ms when `client_token` expires.
- `transport` + `stream_metadata` — the media-plane connection metadata.

```json
{
  "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`:

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

```json
{
  "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:

```bash
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`:

```json
{
  "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.

:::warning[End is not idempotent]
`POST /end` succeeds only on an **active** session. Re-posting against an
already-terminal session returns `409`:

| Terminal state you re-posted against | Error code |
|---|---|
| `ended` / `ended_with_warnings` | `AVTR_SESSION_ENDED` |
| `created` / `ending` / `failed` | `AVTR_SESSION_CONFLICT` |

Design your cleanup to tolerate the 409 — it means the session is already gone.
:::

## 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.

:::danger[One key set per deployment in shared mode]
If your deployment uses a shared legacy key configuration, all keys with the
same tenant identity can access each other's sessions. Production
multi-tenant use requires distinct per-key tenant identities. Ask the platform
team how your keys are configured.
:::

## 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:

```bash
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](/reference/sessions/post-v1-sessions),
[`GET /v1/sessions/{session_id}`](/reference/sessions/get-v1-sessions-session-id),
and [`POST /v1/sessions/{session_id}/end`](/reference/sessions/post-v1-sessions-session-id-end).
