---
title: Quickstart
description: Get from zero to a live avatar session in five minutes — create the session, connect the stream, end it.
sidebar:
  label: Quickstart
  order: 1
---

Create and start a live avatar session in **one API call**, connect your frontend
to the media stream, and end the session from your backend. Everything below uses
the six canonical `/v1/` endpoints — you can copy-paste the whole flow.

:::note[Before you begin]
You need an API key from the platform team. In the examples it is read from the
`AVTR_API_KEY` environment variable — never hardcode it.
:::

## Base URL

All examples use the placeholder below. Replace it with your assigned API host.

```
https://<orvyn-public-api-host>
```

:::warning[Placeholder host]
`<orvyn-public-api-host>` is a **placeholder**. The public API hostname is not
yet finalized — substitute the host you were given when your key was issued.
:::

## The 60-second path

1. **Create and start a session**

    One call creates the session, starts rendering, and returns a short-lived
    `client_token` — delivered **exactly once**.

2. **Connect the WebSocket**

    Build the media WebSocket URL yourself and append the token:
    `wss://<orvyn-public-api-host>/v1/sessions/{session_id}/ws?token=<client_token>`.

3. **Receive `session.ready` + media**

    The server sends `session.ready`, then fMP4 init and media segments.

4. **End the session**

    Clean up from your backend to release GPU resources.

## Step 1 — Create and start a session

The session must specify exactly one `provider_type` and a matching
`provider_config`. Provider credentials are **ephemeral** — your backend mints a
fresh token for every session and Orvyn holds it in memory only for the session
lifetime. Never send provider credentials to the frontend.

**curl**

```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>" }
    }
  }'
```

**JavaScript**

```javascript
const response = await fetch("https://<orvyn-public-api-host>/v1/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AVTR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    provider_type: "openai_realtime",
    provider_config: {
      openai_realtime: { client_secret: "<ephemeral-client-secret>" },
    },
  }),
});
const session = await response.json(); // bare SessionResponse — no envelope
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://<orvyn-public-api-host>/v1/sessions",
    headers={
        "Authorization": f"Bearer {os.environ['AVTR_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "provider_type": "openai_realtime",
        "provider_config": {
            "openai_realtime": {"client_secret": "<ephemeral-client-secret>"}
        },
    },
)
session = response.json()  # bare SessionResponse — no envelope
```

A successful response is a **bare** `SessionResponse` object — no envelope. The
`client_token` is returned **exactly once**, here and nowhere else:

```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": []
  }
}
```

| Field | What it is |
|---|---|
| `session_id` | Stable id, always prefixed with `session:` — save it; every session-scoped call uses it |
| `client_token` | Short-lived (5-minute) HMAC-signed token authorizing media-plane connections — **exactly once**, never in `GET` responses |
| `expires_at` | Unix epoch **milliseconds** when `client_token` expires |
| `transport` | `auto`, `webrtc_sfu`, or `websocket_fmp4` |
| `stream_metadata.session_token` | Purpose-scoped media-plane token — an alternative `?token=` value for the WebSocket |
| `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` |

## Step 2 — Connect the WebSocket

`POST /v1/sessions` already created AND started the session — there is no
separate start step. Build the media WebSocket URL and append the token as the
`?token=` query parameter:

```javascript
const wsUrl = `wss://<orvyn-public-api-host>/v1/sessions/${encodeURIComponent(
  session.session_id,
)}/ws?token=${encodeURIComponent(session.client_token)}`;

const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
// No auth headers — the ?token= value authorizes the connection on its own.

ws.onmessage = (event) => {
  if (typeof event.data !== "string") return; // binary audio frames
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case "session.ready":
      console.log("Avatar ready:", msg.avatarId);
      break;
    case "avatar.fmp4.init":
      console.log("fMP4 init segment received");
      break;
    case "avatar.fmp4.segment":
      console.log("Media segment", msg.sequence);
      break;
    case "session.ended":
      console.log("Session ended");
      break;
    case "error":
      console.error("Server error:", msg.code, msg.message);
      break;
  }
};
```

:::tip[`client_token` vs `stream_metadata.session_token`]
Both are accepted as the `?token=` value. `client_token` is the general-purpose
client token delivered exactly once at creation (and on refresh);
`stream_metadata.session_token` is the purpose-scoped media-plane token. Use
whichever your integration tracks.
:::

The full message catalog — lifecycle, fMP4 media, and speech pipeline messages —
is documented in the [WebSocket guide](/guides/websocket-fmp4).

## Step 3 — End the session

Always end the session from your backend to release resources:

**curl**

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

**JavaScript**

```javascript
await fetch(
  `https://<orvyn-public-api-host>/v1/sessions/${sessionId}/end`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.AVTR_API_KEY}` },
  },
);
```

**Python**

```python
requests.post(
    f"https://<orvyn-public-api-host>/v1/sessions/{session_id}/end",
    headers={"Authorization": f"Bearer {os.environ['AVTR_API_KEY']}"},
)
```

A successful end returns a bare `EndSessionResponse`:

```json
{
  "session_id": "session:8f3c...",
  "status": "ended",
  "ended_at": 1718352182000
}
```

`status` is `ended` after graceful cleanup, or `ended_with_warnings` with an
extra `warning` field when teardown timed out — the session is terminal either
way.

:::warning[409 on already-ended sessions]
`POST /end` only succeeds on an active session. Re-posting against an
already-terminal session returns `409` — it is intentionally **not** idempotent.
`ended` / `ended_with_warnings` map to `AVTR_SESSION_ENDED`; `created` /
`ending` / `failed` map to `AVTR_SESSION_CONFLICT`. See the
[session lifecycle guide](/guides/session-lifecycle#ending-a-session).
:::

:::tip[Stuck teardown? Use `?force=true`]
Append `?force=true` to the end call to skip the graceful disconnect handshake
and terminate backend resources immediately.
:::

## Refreshing the client token

The token expires after **5 minutes**. To reconnect after expiry (network drop,
browser sleep), mint a fresh one:

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

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

Build the reconnect WebSocket URL from the fresh `client_token` (or
`stream_metadata.session_token`), exactly as in step 2. Refreshing does not
revoke previously issued tokens — they remain valid until their own expiry.

## Handle the client token safely

The `?token=` query parameter is a **short-lived bearer credential** (5-minute
TTL). Anyone holding a valid token can connect to the session's WebSocket.

- **Do** build the WebSocket URL with the token and open the connection.
- **Do not** log the token or the assembled URL, screenshot it, or store it in
  analytics or browser history.
- **Do** call `POST /v1/sessions/{session_id}/client-token` for reconnects after
  expiry.

## Next steps

<CardGroup cols={2}>
  <Card title="Web SDK quickstart" href="/guides/web-sdk-quickstart" icon="package">
    Drive the whole flow from the browser with `@avtr/web-sdk`.
  </Card>
  <Card title="Authentication" href="/guides/authentication" icon="lock">
    How API keys and WebSocket tokens work.
  </Card>
  <Card title="Session lifecycle" href="/guides/session-lifecycle" icon="refresh">
    States, ownership, and end semantics.
  </Card>
  <Card title="WebSocket & fMP4" href="/guides/websocket-fmp4" icon="radio">
    The full media protocol and message catalog.
  </Card>
</CardGroup>
