---
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 stream metadata — status `created`.

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:

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

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

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

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

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

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

```json
{
  "success": true,
  "data": {
    "session_id": "session:8f3c...",
    "status": "ended",
    "end_ok": true
  },
  "timings": { "latency_ms": 1820 }
}
```

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

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.

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

## Duplicate and racing calls

The lifecycle is hardened against races:

- A start already in progress makes concurrent lifecycle calls return
  `409 session_starting` until it resolves.
- Ending a session while its start is still in flight cancels the start
  cleanly — the start returns `409 session_cancelled` and 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:

```bash
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](/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).
