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

2. **Connect the WebSocket**

    Open the `ws_url` from the session response — the token is already embedded.

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 { data } = await response.json();
```

**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>"}
        },
    },
)
data = response.json()["data"]
```

A successful response returns the session id, the stream metadata, and the
short-lived `client_token`-backed WebSocket URL:

```json
{
  "success": true,
  "data": {
    "session_id": "session:8f3c1a2b-...",
    "status": "created",
    "stream": {
      "transport": "websocket_fmp4",
      "ws_url": "wss://<orvyn-public-api-host>/v1/sessions/session:.../ws?token=eyJ...",
      "token_expires_at": 1718352300000
    }
  },
  "timings": { "latency_ms": 9 }
}
```

Save the `session_id` — you need it for cleanup. Treat the full `ws_url` as a
bearer credential (see [the security note below](#handle-the-ws_url-safely)).

## Step 2 — Connect the WebSocket

`POST /v1/sessions` already created AND started the session — there is no
separate start step. Open the `ws_url` exactly as returned; the token is already
embedded, so do **not** add auth headers.

```javascript
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";

ws.onopen = () => {
  // No session.start needed — POST /v1/sessions already started the session.
};

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;
  }
};
```

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

:::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.
See the [session lifecycle guide](/guides/session-lifecycle#ending-a-session).
:::

## Refreshing the client token

The token in `ws_url` 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
{
  "success": true,
  "data": {
    "ws_url": "wss://<orvyn-public-api-host>/v1/sessions/session:.../ws?token=eyJ...",
    "token_expires_at": 1718352600000
  },
  "timings": { "latency_ms": 3 }
}
```

Use the newest `ws_url` for reconnects. Refreshing does not revoke previously
issued tokens — they remain valid until their own expiry.

## Handle the ws_url safely

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

- **Do** use the `ws_url` as-is to open the connection.
- **Do not** log it, 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="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>
  <Card title="API reference" href="/reference" icon="file-code">
    The generated contract for all six endpoints.
  </Card>
</CardGroup>
