# Create Session
Source: https://docs.avaturn.live/api-reference/create-session
https://api.avaturn.live/api/v1/openapi.json post /api/v1/sessions
Initiates a session.
Call this endpoint from your backend.
You must not expose your API Key in your frontend code.
Upon successful creation, the API returns a session token that can be safely used in Web SDK.
There are a few ways to process what a user says and what avatar says.
It's configured via the conversation engine. Supported types:
- `openai-realtime` — Avaturn Live talks directly to OpenAI
Realtime on your behalf.
- `cartesia` — [Cartesia Line](https://docs.cartesia.ai/line/integrations/calls-api)
- `external` — Avaturn Live opens a WebSocket to a URL you
provide and exchanges raw audio plus segment events with it.
The reference integration uses [Pipecat](https://docs.pipecat.ai);
see the open-source `pipecat-avaturn-live-demo` repo for a
working end-to-end example.
- `text-echo` — legacy text-driven TTS playback.
Reach out to us if you want to integrate with something else.
# Create Speech Task
Source: https://docs.avaturn.live/api-reference/create-speech-task
https://api.avaturn.live/api/v1/openapi.json post /api/v1/sessions/{id}/tasks
Adds the text to the avatar speech queue.
If the avatar is already speaking, the text fragment is queued to be spoken at a later time.
Nothing is said until you connect or while you're disconnected.
# API Reference
Source: https://docs.avaturn.live/api-reference/introduction
Create, manage, and terminate avatar sessions from your backend.
The Avaturn.Live API manages session lifecycle. Create a session with a [conversation engine](/howtos/openai_realtime_api) config; receive a `session_id` (backend handle) and a `token` (frontend credential).
## Authentication
All endpoints require an `Authorization: Bearer ` header. API keys are issued in the [dashboard](https://avaturn.live/dashboard).
API keys must stay on the backend. Anyone with the key can act on your behalf — rotate immediately in the dashboard if compromised.
## Base URL
```
https://api.avaturn.live
```
## Errors
Endpoint-specific error codes are listed in each operation's reference page. Common patterns:
* `400 Bad Request` — invalid payload (e.g. unknown `voice_id`, invalid `render_model` for the chosen avatar).
* `401 Unauthorized` — missing or revoked API key.
* `410 Gone` — `avatar_id` no longer exists.
* `422 Unprocessable Content` — missing required engine field (e.g. `client_secret` for `openai-realtime`).
# Terminate Session
Source: https://docs.avaturn.live/api-reference/terminate-session
https://api.avaturn.live/api/v1/openapi.json delete /api/v1/sessions/{id}
Terminates the session.
If the avatar is still talking, it kills the session anyway.
# Cartesia conversation engine
Source: https://docs.avaturn.live/howtos/cartesia
Connect an Avaturn avatar to a deployed Cartesia Line agent.
## When to use
* Your conversational agent runs on [Cartesia Line](https://docs.cartesia.ai/line).
* Prompts, tools, voice, and the LLM live inside your Cartesia agent — not in the Avaturn session payload.
* You want natural barge-in detected server-side.
For inline per-session config of prompts/voice/VAD, see [OpenAI Realtime](/howtos/openai_realtime_api).
## Prerequisites
* **Deployed Cartesia Line agent** and its `agent_id` ([quickstart](https://docs.cartesia.ai/line/start-building/quickstart))
* **Cartesia API key** (`sk_car_...`) ([api\_key](https://play.cartesia.ai/keys))
* **Avaturn API key** ([dashboard](https://avaturn.live/dashboard))
## 1. Mint a Cartesia access token
Cartesia Line uses short-lived access tokens for agent connections. Mint server-side from your Cartesia API key — a few minutes is enough.
```python Python theme={null}
import httpx
async with httpx.AsyncClient(timeout=10.0) as http:
r = await http.post(
"https://api.cartesia.ai/access-token",
headers={
"Authorization": "Bearer ",
"Cartesia-Version": "2025-04-16",
},
json={"grants": {"agent": True}, "expires_in": 300},
)
r.raise_for_status()
access_token = r.json()["token"]
```
```javascript Node.js theme={null}
const r = await fetch("https://api.cartesia.ai/access-token", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Cartesia-Version": "2025-04-16",
"Content-Type": "application/json",
},
body: JSON.stringify({ grants: { agent: true }, expires_in: 300 }),
});
if (!r.ok) throw new Error(`Cartesia token: ${r.status}`);
const { token: accessToken } = await r.json();
```
```bash cURL theme={null}
curl https://api.cartesia.ai/access-token \
-H "Authorization: Bearer " \
-H "Cartesia-Version: 2025-04-16" \
-H "Content-Type: application/json" \
-d '{ "grants": { "agent": true }, "expires_in": 300 }'
```
Mint per session. Don't cache. See the [Cartesia authentication guide](https://docs.cartesia.ai/get-started/authenticate-your-client-applications) for scopes.
## 2. Create the Avaturn session
```python Python theme={null}
import httpx
async with httpx.AsyncClient() as http:
r = await http.post(
"https://api.avaturn.live/api/v1/sessions",
headers={"Authorization": "Bearer "},
json={
"conversation_engine": {
"type": "cartesia",
"access_token": access_token,
"agent_id": "",
},
},
)
r.raise_for_status()
session = r.json() # { "session_id": "...", "token": "..." }
```
```javascript Node.js theme={null}
const r = await fetch("https://api.avaturn.live/api/v1/sessions", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_engine: {
type: "cartesia",
access_token: accessToken,
agent_id: "",
},
}),
});
const session = await r.json(); // { session_id, token }
```
```bash cURL theme={null}
curl -X POST https://api.avaturn.live/api/v1/sessions \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"conversation_engine": {
"type": "cartesia",
"access_token": "eyJhbGciOi...",
"agent_id": ""
}
}'
```
Response:
* `session_id` — backend handle
* `token` — short-lived credential for the Web SDK
Optional session fields: `avatar_id`, `background`, `render_model` (avatar render preset, **not** the LLM), `user_absent_timeout` (default 60s, min 10), `max_duration` (default 3600s, max 86400).
## 3. Connect from the frontend
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: session.token,
audioSource: true, // required — engine is voice-to-voice
});
await avatar.init();
```
## Configuring the agent
Cartesia Line is a deployed agent platform: prompts, tools, voice, and the LLM live in your Cartesia agent. To change agent behavior, update and redeploy in Cartesia.
* [Line SDK overview](https://docs.cartesia.ai/line/sdk/overview)
* [Building Line agents](https://docs.cartesia.ai/line/sdk/agents)
* [Deploying Line agents](https://docs.cartesia.ai/line/infrastructure/deployments)
**No per-session variables.** The Avaturn payload accepts only `agent_id` and `access_token`. There's no `variables`, `context`, or `metadata` pass-through. For per-user variation, deploy multiple agents and select the right `agent_id` at session creation.
## Engine behavior
* **Audio.** Avaturn streams the user's microphone to Cartesia as 24 kHz base64-encoded PCM, matching the [Cartesia Calls API](https://docs.cartesia.ai/line/integrations/calls-api) input format.
* **Interruptions.** Cartesia detects barge-in server-side and emits a `clear` signal; Avaturn drops in-flight avatar audio so the next response starts cleanly.
* **No turn boundaries.** Cartesia doesn't emit explicit turn-start / turn-end markers. Avaturn opens a new segment on the first audio chunk and closes it on buffer drain or `clear`.
* **Tools and LLM.** Both execute inside Cartesia's runtime — Avaturn doesn't observe or proxy them. Configure tools in your Cartesia agent.
* **Transcripts.** Cartesia transcripts are not forwarded to the Web SDK. If you need transcripts in your app, capture them inside the Cartesia agent and ship via your own backend.
* **Text input is not played.** The Avaturn `POST /sessions/{id}/tasks` endpoint accepts the request and returns a `task_id`, but the Cartesia engine ignores text-echo commands — the avatar is driven by user voice and your agent logic only.
* **Call transfer is not supported.** If your agent emits a `transfer_call` action, Avaturn logs a warning and ignores it. The avatar continues in the existing session.
* **Server-initiated end.** If your agent invokes the [`end_call`](https://docs.cartesia.ai/line/sdk/agents) tool (or otherwise ends the conversation), Cartesia closes the WebSocket gracefully and the Avaturn session ends as a normal termination.
## Session lifecycle
A session ends on any of:
* Explicit `DELETE /api/v1/sessions/{session_id}`
* Your Cartesia agent ending the call (e.g. via the `end_call` tool)
* `user_absent_timeout` elapses with the user disconnected (default 60s)
* `max_duration` cap reached (default 3600s, max 86400s)
```python theme={null}
async with httpx.AsyncClient() as http:
await http.delete(
f"https://api.avaturn.live/api/v1/sessions/{session_id}",
headers={"Authorization": "Bearer "},
)
```
Call `avatar.dispose()` on the frontend to tear down the local SDK state. The backend session terminates as described above — `dispose()` does not directly close it.
## Reference
* [Cartesia Line quickstart](https://docs.cartesia.ai/line/start-building/quickstart)
* [Cartesia Calls API](https://docs.cartesia.ai/line/integrations/calls-api)
* [Cartesia authentication guide](https://docs.cartesia.ai/get-started/authenticate-your-client-applications)
* [Web SDK integration guide](/web-sdk/integration-guide)
* [Web SDK events](/web-sdk/reference/events)
* [Avaturn API reference](/api-reference/introduction)
# External conversation engine
Source: https://docs.avaturn.live/howtos/external
Drive an Avaturn avatar from your own WebSocket service — bring your own stack.
## When to use
* You want full control over the speech stack — STT, LLM, TTS, turn detection, tools, memory — and run it yourself.
* You already have a Pipecat, LiveKit Agents, custom Python/Node, or other voice agent and need to give it a face.
* The hosted engines ([OpenAI Realtime](/howtos/openai_realtime_api), [Cartesia](/howtos/cartesia)) don't fit your pipeline.
A reference implementation (Pipecat + Pipecat Cloud, OpenAI Realtime or cascaded STT/LLM/TTS) lives at [github.com/avaturn-live/pipecat-avaturn-live-demo](https://github.com/avaturn-live/pipecat-avaturn-live-demo).
## How it works
```mermaid theme={null}
flowchart LR
Browser["Browser
AvaturnHead"]
Avaturn["Avaturn"]
Engine["Your engine
STT · LLM · TTS"]
Browser <== "WebRTC" ==> Avaturn
Avaturn <== "WebSocket
PCM + JSON" ==> Engine
```
When you create a session with `type: "external"`, Avaturn opens a WebSocket to the `url` you provide and exchanges:
* **Binary** — raw PCM16LE mono audio in both directions.
* **JSON** — small control protocol that frames every burst of avatar speech as a *segment* and propagates playback events back to your engine.
Your engine owns the conversation; Avaturn owns the avatar's mouth, eyes, and playback clock.
## Prerequisites
* **A reachable WebSocket endpoint** — `wss://` in production. Avaturn must reach it over the public internet.
* **Avaturn API key** ([dashboard](https://avaturn.live/dashboard)).
* A way to authenticate the incoming WebSocket (shared secret, signed token, IP allowlist — your choice; see [Authentication](#authentication)).
## 1. Create the Avaturn session
```python Python theme={null}
import httpx
async with httpx.AsyncClient() as http:
r = await http.post(
"https://api.avaturn.live/api/v1/sessions",
headers={"Authorization": "Bearer "},
json={
"conversation_engine": {
"type": "external",
"url": "wss://your-engine.example.com/avaturn-live/ws",
"audio": {"user": {"sample_rate": 24000}},
"headers": {"Authorization": "Bearer "},
},
},
)
r.raise_for_status()
session = r.json() # { "session_id": "...", "token": "..." }
```
```javascript Node.js theme={null}
const r = await fetch("https://api.avaturn.live/api/v1/sessions", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_engine: {
type: "external",
url: "wss://your-engine.example.com/avaturn-live/ws",
audio: { user: { sample_rate: 24000 } },
headers: { Authorization: "Bearer " },
},
}),
});
const session = await r.json(); // { session_id, token }
```
```bash cURL theme={null}
curl -X POST https://api.avaturn.live/api/v1/sessions \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"conversation_engine": {
"type": "external",
"url": "wss://your-engine.example.com/avaturn-live/ws",
"audio": { "user": { "sample_rate": 24000 } },
"headers": { "Authorization": "Bearer " }
}
}'
```
`conversation_engine` fields:
| Field | Type | Notes |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `"external"` | Required. |
| `url` | string | `wss://` URL Avaturn opens. Must be reachable from Avaturn's infra. |
| `audio.user.sample_rate` | `16000` \| `24000` | Sample rate of the user-mic stream Avaturn sends you. Default `24000`. Use `24000` for speech-to-speech LLMs that consume audio natively (OpenAI Realtime, Gemini Live). Pick `16000` if you'd rather halve the upstream bitrate — most VAD and turn-detection models (Silero, Smart Turn) work at 16 kHz internally. |
| `headers` | `Record` \| `null` | Optional. Forwarded on the WebSocket upgrade — typically `Authorization: Bearer ...`. The values are stored only for the lifetime of the session. |
Optional top-level session fields: `avatar_id`, `background`, `model` (render model, default `delta`), `user_absent_timeout` (default 60s, min 10), `max_duration` (default 3600s, min 60s, max 86400s). See the [API reference](/api-reference/introduction).
Response:
* `session_id` — backend handle
* `token` — short-lived credential for the Web SDK
## 2. Connect from the frontend
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
// Trigger the mic permission prompt inside the click handler — the SDK
// otherwise calls getUserMedia outside a user gesture and silently fails
// on some browsers.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach((t) => t.stop());
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: session.token,
audioSource: true, // required — engine is voice-to-voice
});
await avatar.init();
```
## 3. Implement the WebSocket protocol
Once the session is created, Avaturn opens a WebSocket to your `url` (with your `headers`) and starts streaming the user's microphone immediately.
### Audio
| Direction | Format |
| ---------------- | ------------------------------------------------ |
| Avaturn → engine | Binary PCM16LE mono @ `audio.user.sample_rate` |
| Engine → Avaturn | Binary PCM16LE mono @ **24 kHz** (avatar speech) |
Resample your TTS output to 24 kHz mono before sending it. Anything else will play back garbled. If your TTS supports native 24 kHz mono output, prefer that over resampling — fewer artifacts and one less CPU step in the hot path.
Chunk size is up to you. 10–40 ms per binary frame works well in practice; Avaturn buffers per segment, so chunk size only affects time-to-first-frame, not playback quality. **Don't throttle output to real-time.** Avaturn Live owns the playback clock and pulls audio as fast as you can produce it — if your framework paces writes by default (some WebSocket transports do), disable that pacing for this socket or segment timing will drift.
### Control messages (engine → Avaturn)
Every chunk of avatar audio must live inside an open **segment**. Open one with `segment.create` before pushing any bytes, then `segment.close` after the last chunk.
```json theme={null}
{ "type": "avatar.speech.segment.create", "segment_uid": "" }
{ "type": "avatar.speech.segment.close", "segment_uid": "" }
{ "type": "avatar.speech.interrupt" }
{ "type": "sdk.message.send", "data": { /* opaque object */ } }
```
* `segment_uid` is your own correlation id (any string). Avaturn echoes it back on the corresponding playback events so you can match them up.
* `avatar.speech.interrupt` discards anything Avaturn has buffered for playback. Use it when your turn detector decides the user has barged in.
* `sdk.message.send` forwards an arbitrary JSON payload to the Web SDK over the data channel (see [Web SDK events](/web-sdk/reference/events)).
Audio sent outside an open segment is dropped and Avaturn replies with an `error` frame — open the segment first.
### Control messages (Avaturn → engine)
```json theme={null}
{ "type": "avatar.speech.segment.created", "segment_id": "...", "segment_uid": "..." }
{ "type": "avatar.speech.segment.closed", "segment_id": "...", "segment_uid": "..." }
{ "type": "avatar.speech.segment.playback.started", "segment_id": "...", "segment_uid": "...", "timestamp": 0.42 }
{ "type": "avatar.speech.segment.playback.ended", "segment_id": "...", "segment_uid": "...", "timestamp": 3.18 }
{ "type": "avatar.speech.segment.playback.interrupted", "segment_id": "...", "segment_uid": "...", "played_duration": 1.07 }
{ "type": "sdk.message.receive", "data": { /* opaque object */ } }
{ "type": "error", "subtype": "...", "reason": "..." }
```
* `segment_id` is Avaturn's id; `segment_uid` is the one you supplied. Use whichever is convenient.
* `playback.started` / `playback.ended` fire when the avatar actually starts/finishes lip-syncing the segment — useful for transcript timing.
* `playback.interrupted` fires after `avatar.speech.interrupt` or when a new user utterance pre-empts the current segment.
* `sdk.message.receive` carries messages sent from the browser via the Web SDK.
`error` frames are advisory — the WebSocket stays open. Most common subtypes:
| `subtype` | Fires when |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `avatar.speech.segment.error` | You pushed audio bytes outside an open segment, or tried to `create` while another was still open. Open / close the segment as expected and retry. |
| `message.type.error` | An incoming JSON frame had an unknown `type`. Check spelling against the outgoing-message list. |
| `json.parsing.error` | An incoming text frame wasn't valid JSON. |
### Segment lifecycle
A correct turn looks like:
```mermaid theme={null}
sequenceDiagram
autonumber
participant E as Your engine
participant A as Avaturn Live
E->>A: avatar.speech.segment.create { segment_uid: "turn-1" }
A->>E: avatar.speech.segment.created { segment_id, segment_uid }
E->>A: (one or many binary frames, 24 kHz mono)
E->>A:
E->>A: avatar.speech.segment.close { segment_uid: "turn-1" }
A->>E: avatar.speech.segment.closed { segment_id, segment_uid }
A->>E: avatar.speech.segment.playback.started { …, timestamp: 0.18 }
A->>E: avatar.speech.segment.playback.ended { …, timestamp: 3.04 }
```
Only one segment can be open at a time. Attempting to `create` while another is open returns an `error` — close the current one first.
## Authentication
Anything reachable on the public internet at a guessable URL is a free avatar — set up auth before exposing the endpoint.
* **Shared secret in `headers`**. Pass `{"Authorization": "Bearer "}` when creating the session and check it in your WS upgrade handler. Simple and good enough for most deployments.
* **Per-session signed token in the URL path**. Mint a short-lived HMAC token at session-create time and bake it into the `url` (e.g. `wss://engine.example.com/ws/`). The token is single-use and self-expiring, so the secret never leaves your infra. This is the pattern the [reference demo](https://github.com/avaturn-live/pipecat-avaturn-live-demo) uses for Pipecat Cloud.
* **IP allowlist**. Contact [support@avaturn.me](mailto:support@avaturn.me) for the current egress range if you want network-level filtering in front of your service.
## Connection behavior
* **Keep-alive.** Avaturn sends WebSocket pings every \~75 seconds with a 30-second pong timeout. Most reverse proxies need an idle-timeout ≥ 180 seconds in front of your engine to avoid mid-conversation drops — bump `proxy_read_timeout` (nginx), idle timeout (ALB, Cloudflare), or the equivalent. You don't need to send application-level pings yourself; Avaturn's WebSocket-protocol pings are sufficient.
* **Disconnect = session end.** If your engine closes the socket, the Avaturn session ends. If Avaturn closes it (e.g. user disconnected, `max_duration` reached), `recv()` returns end-of-stream — drain and exit cleanly.
* **No automatic reconnect.** Avaturn does not retry failed upgrades or dropped connections inside an active session. Make sure your engine is up before the session starts.
## Session lifecycle
A session ends on any of:
* Explicit `DELETE /api/v1/sessions/{session_id}`
* The conversation-engine WebSocket closing
* `user_absent_timeout` elapses with the user disconnected (default 60s)
* `max_duration` cap reached (default 3600s, max 86400s)
```python theme={null}
async with httpx.AsyncClient() as http:
await http.delete(
f"https://api.avaturn.live/api/v1/sessions/{session_id}",
headers={"Authorization": "Bearer "},
)
```
Call `avatar.dispose()` on the frontend to tear down the local SDK state. The backend session terminates as described above — `dispose()` does not directly close it.
## Reference implementation
[**github.com/avaturn-live/pipecat-avaturn-live-demo**](https://github.com/avaturn-live/pipecat-avaturn-live-demo) — a full open-source reference. Two pipelines ship side-by-side, switchable via a single env var: speech-to-speech (OpenAI Realtime) and cascaded (STT → LLM → TTS). Same transport, serializer, and segment processor wrap both.
* `pipecat_avaturn/serializer.py` — bidirectional Pipecat ↔ Avaturn wire format. Read this first to see the protocol on the wire.
* `pipecat_avaturn/segment_processor.py` — `TTSStartedFrame` / `TTSStoppedFrame` → `segment.create` / `segment.close`.
* `pipecat_avaturn/transport.py` — the Pipecat FastAPI WebSocket transport with its default real-time pacing sleep disabled. The non-obvious gotcha for anyone building a streaming engine — see the "Don't throttle output" note in [Audio](#audio).
* `pipecat_avaturn/broker.py` — minimal client for `POST /api/v1/sessions` with `type: "external"`.
* `server.py` — FastAPI app combining the session broker and the conversation engine in one process.
Fork it, swap in your own STT/LLM/TTS, and you have a production-shaped Avaturn integration in an afternoon.
## See also
* [Web SDK integration guide](/web-sdk/integration-guide)
* [Web SDK events](/web-sdk/reference/events)
* [Avaturn API reference](/api-reference/introduction)
* [OpenAI Realtime engine](/howtos/openai_realtime_api) — hosted alternative.
* [Cartesia engine](/howtos/cartesia) — hosted alternative.
# OpenAI Realtime engine
Source: https://docs.avaturn.live/howtos/openai_realtime_api
Voice-to-voice avatar powered by the OpenAI Realtime API.
## When to use
* Low-latency voice-to-voice with natural barge-in.
* Prompts, voice, and turn detection configured **inline per session** via an ephemeral OpenAI client secret.
* You don't want to run a separate agent runtime.
For Cartesia Line-managed agents, see [Cartesia](/howtos/cartesia). For backend-driven scripted speech, see [Legacy text-echo](/legacy/text-echo).
## Prerequisites
* **OpenAI API key** with Realtime API access. Only the **GA** version is supported — the beta is not.
* **Avaturn API key** ([dashboard](https://avaturn.live/dashboard)).
## 1. Mint a client secret
On your backend, exchange your OpenAI API key for a short-lived client secret. Avaturn uses this secret to open the Realtime WebSocket on the user's behalf.
```python Python theme={null}
from openai import AsyncOpenAI
openai = AsyncOpenAI(api_key="")
secret = await openai.realtime.client_secrets.create(
expires_after={"seconds": 600, "anchor": "created_at"},
session={"type": "realtime", "model": "gpt-realtime"},
)
client_secret = secret.value # ek_...
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: "" });
const secret = await openai.realtime.clientSecrets.create({
expires_after: { seconds: 600, anchor: "created_at" },
session: { type: "realtime", model: "gpt-realtime" },
});
const clientSecret = secret.value; // ek_...
```
```bash cURL theme={null}
curl https://api.openai.com/v1/realtime/client_secrets \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"expires_after": { "seconds": 600, "anchor": "created_at" },
"session": { "type": "realtime", "model": "gpt-realtime" }
}'
```
Mint a fresh secret per user session. The default lifetime is 600 seconds (max 7200). The secret governs token issuance — an existing WebSocket continues working after the secret expires.
## 2. Create an Avaturn session
```python Python theme={null}
import httpx
async with httpx.AsyncClient() as http:
r = await http.post(
"https://api.avaturn.live/api/v1/sessions",
headers={"Authorization": "Bearer "},
json={
"conversation_engine": {
"type": "openai-realtime",
"client_secret": client_secret,
},
},
)
r.raise_for_status()
session = r.json() # { "session_id": "...", "token": "..." }
```
```javascript Node.js theme={null}
const r = await fetch("https://api.avaturn.live/api/v1/sessions", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_engine: {
type: "openai-realtime",
client_secret: clientSecret,
},
}),
});
const session = await r.json(); // { session_id, token }
```
```bash cURL theme={null}
curl -X POST https://api.avaturn.live/api/v1/sessions \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"conversation_engine": {
"type": "openai-realtime",
"client_secret": "ek_xxxxxxxxxxxx"
}
}'
```
Response:
* `session_id` — backend handle (terminate, telemetry)
* `token` — short-lived credential for the Web SDK
Optional session fields: `avatar_id`, `background`, `render_model` (avatar render preset, **not** the LLM), `user_absent_timeout` (default 60s, min 10), `max_duration` (default 3600s, max 86400). See the [API reference](/api-reference/create-session).
## 3. Connect from the frontend
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: session.token,
audioSource: true, // required — voice-to-voice
});
await avatar.init();
```
## Configuring the agent
The `session` object you pass to `client_secrets.create()` is applied to the WebSocket Avaturn opens on the user's behalf — full control over instructions, voice, VAD, and transcription.
### Instructions and voice
```python theme={null}
secret = await openai.realtime.client_secrets.create(
expires_after={"seconds": 600, "anchor": "created_at"},
session={
"type": "realtime",
"model": "gpt-realtime",
"instructions": "You are a helpful assistant. Be concise and friendly.",
"audio": {
"output": {"voice": "marin"},
"input": {
"transcription": {"model": "whisper-1"},
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"silence_duration_ms": 500,
},
},
},
},
)
```
OpenAI currently recommends `marin` and `cedar` voices for best quality. Other supported values: `alloy`, `ash`, `ballad`, `coral`, `echo`, `sage`, `shimmer`, `verse`.
**User transcripts require `audio.input.transcription`.** Without it, OpenAI doesn't emit transcription events and Avaturn has nothing to forward to the SDK. Avatar response transcripts (assistant side) flow regardless.
### Stored prompts
```python theme={null}
secret = await openai.realtime.client_secrets.create(
expires_after={"seconds": 600, "anchor": "created_at"},
session={
"type": "realtime",
"model": "gpt-realtime",
"prompt": {
"id": "pmpt_abc123",
"version": "6",
"variables": {"company_name": "Acme", "tone": "professional"},
},
},
)
```
Full configuration surface (turn detection variants, transcription, audio params): [OpenAI session reference](https://platform.openai.com/docs/api-reference/realtime-sessions).
## Engine behavior
* **Audio.** 24 kHz mono PCM in both directions.
* **Interruptions.** OpenAI server VAD (or semantic VAD, if configured). When the user starts speaking, Avaturn discards in-flight avatar audio.
* **Transcripts.** Assistant transcripts (`response.output_audio_transcript.done`) flow by default. User transcripts (`conversation.item.input_audio_transcription.completed`) flow only when `audio.input.transcription` is configured. Both are forwarded to the SDK via [`ce_events.realtime.*`](/web-sdk/reference/events#transcripts).
* **Tools.** Tool definitions sent in the session config are parsed by OpenAI, but Avaturn doesn't surface `response.function_call_arguments.*` events to the Web SDK nor relay function results back. Tool calls won't execute end-to-end — avoid them at this layer until proper support lands.
* **GA only.** Beta or mixed beta/GA usage causes a `session_lifecycle_error` with code `openai-realtime-version-mismatch`. See [beta-to-GA migration](https://platform.openai.com/docs/guides/realtime#beta-to-ga-migration).
## Session lifecycle
A session ends on any of:
* Explicit `DELETE /api/v1/sessions/{session_id}`
* `user_absent_timeout` elapses with the user disconnected (default 60s)
* `max_duration` cap reached (default 3600s, max 86400s)
```python theme={null}
async with httpx.AsyncClient() as http:
await http.delete(
f"https://api.avaturn.live/api/v1/sessions/{session_id}",
headers={"Authorization": "Bearer "},
)
```
Call `avatar.dispose()` on the frontend to tear down the local SDK state. The backend session terminates as described above — `dispose()` does not directly close it. Don't try to resume a session after it ends; mint a new client secret and create a fresh session.
## Reference
* [OpenAI Realtime guide](https://platform.openai.com/docs/guides/realtime)
* [OpenAI session config reference](https://platform.openai.com/docs/api-reference/realtime-sessions)
* [Web SDK integration guide](/web-sdk/integration-guide)
* [Web SDK events](/web-sdk/reference/events)
* [Avaturn API reference](/api-reference/introduction)
# Introduction
Source: https://docs.avaturn.live/introduction
Real-time conversational video avatars for the web.
Avaturn.Live renders a photo-realistic avatar driven by a real-time conversation engine. Mint a session on your backend, pass a short-lived token to the frontend, and the [Web SDK](/web-sdk/integration-guide) streams the avatar over WebRTC.
## How it works
[`POST /api/v1/sessions`](/api-reference/create-session) with a conversation engine config. Returns `session_id` (backend) and `token` (frontend).
Pass `token` to the [Web SDK](/web-sdk/integration-guide). The avatar joins over WebRTC and starts conversing.
[`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session), or let it expire on idle.
## Conversation engines
| Engine | When | Configured |
| -------------------------------------------------- | ----------------------------------------------- | -------------------------------------- |
| [**OpenAI Realtime**](/howtos/openai_realtime_api) | Low-latency voice-to-voice with inline prompts | Per session, ephemeral client secret |
| [**Cartesia**](/howtos/cartesia) | Voice-to-voice agents deployed on Cartesia Line | Per agent, in your Cartesia deployment |
New integrations should use [OpenAI Realtime](/howtos/openai_realtime_api) — it's the fastest path to a working voice-to-voice avatar. Cartesia is the right pick if your agent already lives on Cartesia Line.
A legacy text-echo flow (backend pushes text, avatar speaks it) is preserved under [Legacy](/legacy/text-echo) for existing integrations.
## Start here
Voice-to-voice avatar in 5 minutes.
Render, devices, events, lifecycle.
Session lifecycle from your backend.
Endpoints, schemas, errors.
# Streaming LLM output (legacy)
Source: https://docs.avaturn.live/legacy/llm-streaming
Legacy: pipe an LLM stream into the text-echo task endpoint. Use a conversation engine instead.
**Legacy.** This pattern wraps an LLM around the [text-echo flow](/legacy/text-echo). New integrations should use [OpenAI Realtime](/howtos/openai_realtime_api) or [Cartesia](/howtos/cartesia) — both handle LLM, TTS, and interruption end-to-end.
To stream LLM responses into a text-echo avatar:
1. Use your LLM's streaming endpoint.
2. Split the stream into sentences as it arrives.
3. Send each sentence to the Avaturn [`POST /api/v1/sessions/{id}/tasks`](/api-reference/create-speech-task) endpoint (or call [`avatar.task()`](/web-sdk/reference/methods#task-text) from the frontend).
For per-sentence chunking heuristics, see ElevenLabs' [streaming TTS WebSocket guide](https://elevenlabs.io/docs/cookbooks/voices/streaming-via-websocket) — the sentence-split pattern applies here too.
# React demo (legacy)
Source: https://docs.avaturn.live/legacy/react-example
React integration with Echo / GPT-4 text-echo flow.
**Legacy.** This demo uses the [text-echo flow](/legacy/text-echo) (avatar speaks text you push to it). For a voice-to-voice integration, follow the [Quickstart](/quickstart) instead.
The [example-react demo](https://github.com/avaturn-live/example-react) integrates OpenAI's chat completions with the Avaturn.Live SDK over the legacy text-echo flow. Users submit text inputs that either trigger an echo from the avatar or stream a GPT-4 response back through the avatar.
## Components
* **[OpenAI hook](https://github.com/avaturn-live/example-react/blob/main/src/hooks/use-openai.hook.tsx)** — manages the OpenAI client and streams responses.
* **[TokenPopup](https://github.com/avaturn-live/example-react/blob/main/src/components/token-popup.tsx)** — modal for entering the OpenAI API token.
* **[SettingsForm](https://github.com/avaturn-live/example-react/blob/main/src/components/settings-form.tsx)** — switches between Echo and OpenAI mode and captures API keys.
* **[Home](https://github.com/avaturn-live/example-react/blob/main/src/pages/index.tsx)** — mounts the avatar video and the input form.
## Flow
1. The user picks a mode (Echo or OpenAI) and provides API keys via `SettingsForm`.
2. The avatar is initialized with the Avaturn.Live SDK.
3. On submit, text is either passed straight to `avatar.task(text)` (echo) or streamed through OpenAI; each sentence chunk is pushed to `avatar.task()` as it arrives (see [Streaming LLM output](/legacy/llm-streaming)).
# Text echo (legacy)
Source: https://docs.avaturn.live/legacy/text-echo
Legacy server-driven speech: push text, avatar speaks it. Not recommended for new integrations.
**Legacy / not recommended.** Text echo is preserved for backward compatibility only. It has **no STT**, no built-in interruption, and you handle the LLM pipeline yourself. New integrations should use a conversation engine — [OpenAI Realtime](/howtos/openai_realtime_api) or [Cartesia](/howtos/cartesia).
## When this still makes sense
Use text echo only if you need:
* Fully scripted, deterministic speech with no STT
* Backend-driven LLM output where you stream sentences to the avatar yourself
* Compatibility with existing integrations that already depend on this flow
For anything voice-to-voice, [OpenAI Realtime](/howtos/openai_realtime_api) or [Cartesia](/howtos/cartesia) is the right choice.
## How it works
[`POST /api/v1/sessions`](/api-reference/create-session) with an explicit `text-echo` engine config, or omit `conversation_engine` to inherit the avatar's default text-echo voice. The avatar stays silent until you push text.
From your backend: [`POST /api/v1/sessions/{id}/tasks`](/api-reference/create-speech-task). From the frontend: [`avatar.task(text)`](/web-sdk/reference/methods#task-text) on the Web SDK instance.
[`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session), or wait for `user_absent_timeout`.
## Engine config
To override the default voice, pass an explicit `text-echo` engine config when creating the session:
```json theme={null}
{
"conversation_engine": {
"type": "text-echo",
"tts": {
"engine": "elevenlabs",
"voice_id": ""
}
}
}
```
Only `elevenlabs` is currently accepted as the TTS engine. `tts` is required when `type` is `text-echo`. The `voice_id` is validated against ElevenLabs at session creation — an invalid id returns `HTTP 400`.
## Backend example
```python theme={null}
import asyncio
import httpx
from pydantic import BaseModel
class CreateSessionResponse(BaseModel):
session_id: str
token: str
class SessionSayResponse(BaseModel):
task_id: str
class APIClient:
def __init__(self, api_key: str, base_url: str = "https://api.avaturn.live") -> None:
self.headers = {"Authorization": f"Bearer {api_key}"}
self.base_url = base_url
async def create_session(self) -> CreateSessionResponse:
async with httpx.AsyncClient() as http:
r = await http.post(f"{self.base_url}/api/v1/sessions", headers=self.headers)
return CreateSessionResponse.model_validate(r.json())
async def say(self, session_id: str, text: str) -> SessionSayResponse:
async with httpx.AsyncClient() as http:
r = await http.post(
f"{self.base_url}/api/v1/sessions/{session_id}/tasks",
json={"text": text},
headers=self.headers,
)
return SessionSayResponse.model_validate(r.json())
async def terminate_session(self, session_id: str) -> None:
async with httpx.AsyncClient() as http:
await http.delete(
f"{self.base_url}/api/v1/sessions/{session_id}",
headers=self.headers,
)
async def main() -> None:
client = APIClient(api_key="")
session = await client.create_session()
# send session.token to the frontend
await client.say(session.session_id, "Hello, world!")
await asyncio.sleep(3)
await client.terminate_session(session.session_id)
asyncio.run(main())
```
## Frontend example
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: "",
});
await avatar.init();
await avatar.task("Some text to say");
// queued behind any in-progress utterance; nothing speaks while disconnected
```
Related SDK methods (also legacy): [`task()`, `cancelAllTasks()`, `changeVoice()`](/web-sdk/reference/methods#legacy-text-echo).
## Streaming LLM output
To feed an LLM stream through `task()`, see [Streaming LLM output](/legacy/llm-streaming).
## React demo
The legacy [React demo](/legacy/react-example) shows this flow end-to-end with an Echo / GPT-4 toggle.
# Quickstart
Source: https://docs.avaturn.live/quickstart
Voice-to-voice avatar in 5 minutes with OpenAI Realtime.
You'll mint an ephemeral OpenAI client secret on your backend, create an Avaturn session bound to it, and connect from the browser. The conversation engine is [OpenAI Realtime](/howtos/openai_realtime_api).
## Prerequisites
* **Avaturn API key** ([dashboard](https://avaturn.live/dashboard))
* **OpenAI API key** with Realtime API access
* Backend in Node.js or Python; any JS framework on the frontend
## 1. Mint an OpenAI client secret
On your backend, exchange your OpenAI API key for a short-lived client secret.
```python Python theme={null}
from openai import AsyncOpenAI
openai = AsyncOpenAI(api_key="")
secret = await openai.realtime.client_secrets.create(
expires_after={"seconds": 600, "anchor": "created_at"},
session={
"type": "realtime",
"model": "gpt-realtime",
"instructions": "You are a friendly assistant. Keep replies brief.",
"audio": {"output": {"voice": "marin"}},
},
)
client_secret = secret.value # ek_...
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: "" });
const secret = await openai.realtime.clientSecrets.create({
expires_after: { seconds: 600, anchor: "created_at" },
session: {
type: "realtime",
model: "gpt-realtime",
instructions: "You are a friendly assistant. Keep replies brief.",
audio: { output: { voice: "marin" } },
},
});
const clientSecret = secret.value; // ek_...
```
## 2. Create an Avaturn session
```python Python theme={null}
import httpx
async with httpx.AsyncClient() as http:
r = await http.post(
"https://api.avaturn.live/api/v1/sessions",
headers={"Authorization": "Bearer "},
json={
"conversation_engine": {
"type": "openai-realtime",
"client_secret": client_secret,
}
},
)
r.raise_for_status()
session = r.json() # { "session_id": "...", "token": "..." }
```
```javascript Node.js theme={null}
const resp = await fetch("https://api.avaturn.live/api/v1/sessions", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_engine: {
type: "openai-realtime",
client_secret: clientSecret,
},
}),
});
const session = await resp.json(); // { session_id, token }
```
Never expose `` or `` to the browser. Only the per-session `token` belongs there.
## 3. Connect from the frontend
```bash theme={null}
npm install @avaturn-live/web-sdk
```
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: session.token,
audioSource: true,
});
await avatar.init();
```
The avatar joins the room, requests microphone access, and starts conversing. Speak into the mic — the avatar responds.
## 4. Clean up
```typescript theme={null}
await avatar.dispose();
```
`dispose()` tears down the local SDK state. The backend session expires shortly after the user disconnects (default 60s). To terminate immediately, call [`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session) from your backend.
## Next steps
Tools, custom prompts, turn detection, transcripts.
Drive the avatar from a Cartesia Line agent.
Devices, mute, attach to a new DOM node.
Speech start/end, lifecycle, transcripts.
# Example
Source: https://docs.avaturn.live/rest/example
End-to-end backend session lifecycle with the OpenAI Realtime engine.
Mint an OpenAI ephemeral client secret, create an Avaturn session bound to it, and hand the session token to your frontend. Engine config details: [OpenAI Realtime](/howtos/openai_realtime_api).
```python theme={null}
import asyncio
import httpx
from openai import AsyncOpenAI
from pydantic import BaseModel
AVATURN_API_KEY = ""
OPENAI_API_KEY = ""
class CreateSessionResponse(BaseModel):
session_id: str
token: str
class AvaturnClient:
def __init__(self, api_key: str, base_url: str = "https://api.avaturn.live") -> None:
self.headers = {"Authorization": f"Bearer {api_key}"}
self.base_url = base_url
async def create_session(self, client_secret: str) -> CreateSessionResponse:
async with httpx.AsyncClient() as http:
r = await http.post(
f"{self.base_url}/api/v1/sessions",
headers=self.headers,
json={
"conversation_engine": {
"type": "openai-realtime",
"client_secret": client_secret,
}
},
)
r.raise_for_status()
return CreateSessionResponse.model_validate(r.json())
async def terminate_session(self, session_id: str) -> None:
async with httpx.AsyncClient() as http:
await http.delete(
f"{self.base_url}/api/v1/sessions/{session_id}",
headers=self.headers,
)
async def main() -> None:
# 1. Mint a short-lived OpenAI client secret
openai = AsyncOpenAI(api_key=OPENAI_API_KEY)
secret = await openai.realtime.client_secrets.create(
expires_after={"seconds": 600, "anchor": "created_at"},
session={"type": "realtime", "model": "gpt-realtime"},
)
# 2. Create an Avaturn session bound to the OpenAI secret
avaturn = AvaturnClient(AVATURN_API_KEY)
session = await avaturn.create_session(client_secret=secret.value)
print(session.model_dump()) # send session.token to the frontend
# 3. ... user converses with the avatar via the Web SDK ...
# 4. Terminate explicitly (or rely on auto-termination)
await avaturn.terminate_session(session.session_id)
asyncio.run(main())
```
**Cartesia variant.** Replace the OpenAI minting step with the Cartesia access token flow, then set the engine config to `{ "type": "cartesia", "access_token": "...", "agent_id": "..." }`. Full walkthrough: [Cartesia engine](/howtos/cartesia).
# REST Integration Guide
Source: https://docs.avaturn.live/rest/integration-guide
Session lifecycle from your backend.
Run the REST API from your backend. Avaturn API keys exposed in the browser can be stolen and used to act on your behalf.
## Session lifecycle
[`POST /api/v1/sessions`](/api-reference/create-session) with a [conversation engine](/howtos/openai_realtime_api) config. Response contains `session_id` (backend) and `token` (frontend).
The `token` is the only credential the browser needs. The [Web SDK](/web-sdk/integration-guide) uses it to join the avatar room over WebRTC.
[`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session) when you're done, or rely on auto-termination (`user_absent_timeout` / `max_duration`).
A working end-to-end snippet lives in [Example](/rest/example).
For backend-driven scripted speech (no STT), see [Legacy text-echo](/legacy/text-echo).
# Web SDK Integration Guide
Source: https://docs.avaturn.live/web-sdk/integration-guide
Mount the avatar in your frontend and wire up events.
The Web SDK renders an Avaturn avatar into a DOM element and bridges audio between the user's microphone and the active [conversation engine](/howtos/openai_realtime_api).
## 1. Install
```bash theme={null}
npm install @avaturn-live/web-sdk
```
## 2. Initialize
Create an `AvaturnHead` with a session token minted on your backend ([`POST /api/v1/sessions`](/api-reference/create-session)).
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: "",
audioSource: true, // request mic for voice-to-voice engines
});
await avatar.init();
```
Make sure the DOM node exists before construction. In React/Vue use a `ref` and construct inside an effect; in Angular use `@ViewChild` and construct in `ngAfterViewInit`.
All constructor options: [Properties](/web-sdk/reference/properties).
## 3. Subscribe to events
```typescript theme={null}
avatar.on("avatar_started_speaking", () => {
// e.g. show "speaking" indicator
});
avatar.on("avatar_ended_speaking", () => {
// e.g. hide indicator
});
```
Full list: [Events](/web-sdk/reference/events).
## 4. Dispose
```typescript theme={null}
await avatar.dispose();
```
`dispose()` tears down local SDK state. The backend session terminates via `user_absent_timeout` after disconnection — to end it immediately call [`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session).
## Under the hood
The SDK talks to Avaturn through a private endpoint prefix `${apiHost}/_sdk/v0` (default host `https://api.avaturn.live`) authenticated with an `X-Session-Token` header. The session-room handshake is delegated to [Daily.co](https://daily.co/) over WebRTC.
For self-hosted deployments override `apiHost` ([Properties](/web-sdk/reference/properties#apihost)) and make sure both `${host}/api/v1` (public REST) and `${host}/_sdk/v0` (SDK-only) resolve.
## Driving speech manually (legacy)
If you push text from a backend instead of running a conversation engine, see [Legacy text-echo](/legacy/text-echo).
# Events
Source: https://docs.avaturn.live/web-sdk/reference/events
Events emitted by the AvaturnHead instance.
Subscribe with [`on(event, callback)`](/web-sdk/reference/methods#on-event-callback-off-event-callback) and unsubscribe with `off(event, callback)`.
The SDK forwards events from two sources:
* **Avatar / engine events** sent by the Avaturn backend over the underlying transport (avatar speech, transcripts, errors).
* **Client lifecycle events** emitted locally by the SDK (`init`, `idle`, microphone state).
## Avatar speech
### `avatar_started_speaking`
Fired when the avatar starts speaking.
```typescript theme={null}
{ type: "avatar_started_speaking", phrase_id: string }
```
### `avatar_ended_speaking`
Fired when the avatar finishes the current utterance.
```typescript theme={null}
{ type: "avatar_ended_speaking" }
```
### `avatar_started_speaking_phrase`
Fired when the avatar starts a specific queued phrase. Use this when you need to track individual phrases (e.g. for captioning).
```typescript theme={null}
{ type: "avatar_started_speaking_phrase", phrase_id: string }
```
### `avatar_ended_speaking_phrase`
Fired when the avatar finishes a phrase.
```typescript theme={null}
{ type: "avatar_ended_speaking_phrase", phrase_id: string }
```
## Transcripts
Emitted by the [OpenAI Realtime engine](/howtos/openai_realtime_api). Not emitted by the [Cartesia engine](/howtos/cartesia).
**User-input transcripts require configuration.** Set `audio.input.transcription.model` in the OpenAI session config (e.g. `"whisper-1"`); without it the input transcript event never fires. Assistant transcripts flow by default.
### `ce_events.realtime.input_transcript`
User speech transcription.
```typescript theme={null}
{
type: "ce_events.realtime.input_transcript",
transcript: string,
timestamp: number // unix seconds
}
```
### `ce_events.realtime.response_transcript`
Avatar (assistant) speech transcription.
```typescript theme={null}
{
type: "ce_events.realtime.response_transcript",
transcript: string,
timestamp: number // unix seconds
}
```
## Client lifecycle
Emitted by the SDK locally.
### `init`
Fired when the avatar video track is ready. The promise returned by [`init()`](/web-sdk/reference/methods#init) resolves on this event.
### `idle`
Fired after 5 minutes without a backend message or [`task()`](/web-sdk/reference/methods#task-text) call. If [`keepAlive`](/web-sdk/reference/properties#keepalive) is `false`, the SDK also tears down its local state right after the event fires.
The `idle` event bypasses the [`event`](#event) catch-all. Subscribe to `idle` directly if you need to react to it.
## Errors
### `error`
Fired when the backend reports a lifecycle failure. Payload structure:
```typescript theme={null}
{
type: "error",
error: {
type: "session_lifecycle_error",
code: "internal_error" | "limits_exceeded" | "openai-realtime-version-mismatch",
message: string,
session_id: string
}
}
```
* `openai-realtime-version-mismatch` — your OpenAI session config mixes beta and GA-only features. Use GA-only ([beta-to-GA migration](https://platform.openai.com/docs/guides/realtime#beta-to-ga-migration)).
* `limits_exceeded` — workspace quota reached.
* `internal_error` — generic server-side failure; check `message` for details.
## Microphone
### `local_audio_change`
Fired when the user's microphone mute state changes. Callback receives a `boolean` directly (`true` = unmuted, `false` = muted).
```typescript theme={null}
avatar.on("local_audio_change", (active: boolean) => {
// ...
});
```
### `local_mic_state_change`
Fired when the microphone permission or device state changes. Callback receives a state string from Daily's [`DailyTrackState['state']`](https://docs.daily.co/reference/daily-js/types/daily-track-state) union (e.g. `"blocked"`, `"off"`, `"sendable"`, `"loading"`, `"interrupted"`, `"playable"`, `"receivable"`).
```typescript theme={null}
avatar.on("local_mic_state_change", (state) => {
// ...
});
```
## Catch-all
### `event`
Fires alongside every other event **except** [`idle`](#idle). Useful for logging or debugging.
```typescript theme={null}
avatar.on("event", ({ event, value }) => {
console.log(event, value);
});
```
## Example
```typescript theme={null}
function onPhraseEnd({ phrase_id }: { phrase_id: string }) {
console.log("phrase done:", phrase_id);
}
avatar.on("avatar_ended_speaking_phrase", onPhraseEnd);
// later
avatar.off("avatar_ended_speaking_phrase", onPhraseEnd);
```
# Methods
Source: https://docs.avaturn.live/web-sdk/reference/methods
Methods exposed by the AvaturnHead instance.
## Lifecycle
### `init()`
Initializes the session and connects to the avatar room. Returns a Promise that resolves when the avatar's video track is ready, or rejects on failure. If `immediatelyJoin` is `true` (default), `init()` calls [`join()`](#join) automatically.
```javascript theme={null}
try {
await avatar.init();
} catch (e) {
console.error("Avatar init failed:", e);
}
```
### `join()`
Connects to the avatar room. Called automatically by `init()` unless `immediatelyJoin: false`. Use this when you want manual control over the moment of connection.
Start with microphone muted. Defaults to the SDK config's `startAudioOff`.
```javascript theme={null}
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, { sessionToken, immediatelyJoin: false });
await avatar.init();
// ...user interaction...
await avatar.join({ startAudioOff: false });
```
### `dispose()`
Terminates local SDK state — destroys the Daily call object and aborts pending requests. Does **not** close the backend session directly; the backend session ends via `user_absent_timeout` (default 60s) or an explicit [`DELETE /api/v1/sessions/{id}`](/api-reference/terminate-session). Call this on unmount.
```javascript theme={null}
await avatar.dispose();
```
### `attachDOMNode(node)`
Moves the avatar video into a different DOM element without re-initializing. Also use this when you constructed `AvaturnHead` without a DOM element and now want to render it. Takes effect after `init()` has resolved.
New container for the avatar video.
```javascript theme={null}
avatar.attachDOMNode(document.getElementById("avatar-container")!);
```
## Events
### `on(event, callback)` / `off(event, callback)`
Subscribe to or unsubscribe from SDK events. See [Events](/web-sdk/reference/events) for the full list.
Event name.
Handler invoked when the event fires.
```javascript theme={null}
function onInit() { console.log("avatar ready"); }
avatar.on("init", onInit);
avatar.off("init", onInit);
```
## Media devices
### `requestMediaDevices()`
Returns the list of available media devices (microphones, speakers, cameras).
```javascript theme={null}
const { devices } = await avatar.requestMediaDevices();
const microphones = devices.filter(d => d.kind === "audioinput");
```
### `setInputDevices({ audioDeviceId?, videoDeviceId? })`
Selects input devices for the session. Pass only the keys you want to change — both fields are optional at runtime. Use `false` or `null` to disable a device.
Microphone device id from `requestMediaDevices()`.
Camera device id. Most integrations leave this unset — the avatar's video comes from the server, not the user's camera.
```javascript theme={null}
await avatar.setInputDevices({ audioDeviceId: "microphone-id-123" });
```
Returns [`DailyDeviceInfos`](https://docs.daily.co/reference/daily-js/instance-methods/set-input-devices-async) from the underlying transport.
### `setOutputDevice({ outputDeviceId })`
Selects the speaker for avatar audio output. Always pass `outputDeviceId`.
Device id from `requestMediaDevices()`.
```javascript theme={null}
await avatar.setOutputDevice({ outputDeviceId: "speaker-id-123" });
```
If `outputDeviceId` is omitted, the SDK silently returns the list of input devices instead of switching output. Always pass `outputDeviceId` explicitly.
Returns [`DailyDeviceInfos`](https://docs.daily.co/reference/daily-js/instance-methods/set-input-devices-async).
### `toggleLocalAudio(value?)`
Mutes or unmutes the user's microphone. With no argument, toggles the current state.
`true` = unmute, `false` = mute.
```javascript theme={null}
avatar.toggleLocalAudio(); // toggle
avatar.toggleLocalAudio(false); // mute
```
***
## Legacy: text-echo
**Legacy.** These methods belong to the [text-echo flow](/legacy/text-echo). New integrations should drive speech through a [conversation engine](/howtos/openai_realtime_api) instead.
### `task(text)`
Sends text for the avatar to speak. If the avatar is already speaking, the fragment is queued. Returns `{ task_id: string }`.
The text to speak.
```javascript theme={null}
const { task_id } = await avatar.task("Hello, world!");
```
### `cancelAllTasks()`
Stops the current utterance and clears the queue.
```javascript theme={null}
await avatar.cancelAllTasks();
```
### `changeVoice(config)`
Switches the text-echo TTS voice for subsequent `task()` calls. Patches the session's conversation engine config to a new `text-echo` config with the supplied TTS.
`{ engine: "elevenlabs", voice_id: string }`. Only `elevenlabs` is currently accepted by the backend.
```javascript theme={null}
await avatar.changeVoice({ engine: "elevenlabs", voice_id: "voice_id_123" });
```
# Properties
Source: https://docs.avaturn.live/web-sdk/reference/properties
Constructor signature and configuration options for AvaturnHead.
## Constructor
Two overloads:
```typescript theme={null}
new AvaturnHead(options: AvaturnHeadConfig)
new AvaturnHead(rootElement: HTMLElement, options: AvaturnHeadConfig)
```
Pass a DOM element as the first argument to render the avatar into it on `init()`. Omit it and the avatar isn't rendered until you call [`attachDOMNode()`](/web-sdk/reference/methods#attachdomnode).
```typescript theme={null}
import { AvaturnHead } from "@avaturn-live/web-sdk";
const root = document.querySelector("#avaturn-video")!;
const avatar = new AvaturnHead(root, {
sessionToken: "",
audioSource: true,
immediatelyJoin: true,
startAudioOff: false,
preloadBundle: false,
preconnect: false,
keepAlive: false,
apiHost: "https://api.avaturn.live",
});
await avatar.init();
```
Optional first positional argument. When passed, the avatar renders into this element on `init()`. Without it, call [`attachDOMNode()`](/web-sdk/reference/methods#attachdomnode) before rendering becomes visible.
## Config
Per-session token from [`POST /api/v1/sessions`](/api-reference/create-session). Must be minted on your backend.
Requests microphone access and routes the user's voice into the conversation engine. Required for voice-to-voice flows ([OpenAI Realtime](/howtos/openai_realtime_api), [Cartesia](/howtos/cartesia)).
Starts the session with the microphone muted. Defaults to the opposite of `audioSource` — voice-to-voice sessions start unmuted, text-echo sessions start muted.
Joins the avatar room as soon as `init()` resolves. Set to `false` if you need a manual gap between init and join — then call [`join()`](/web-sdk/reference/methods#join) yourself.
Fetches the SDK runtime bundle eagerly. Reduces first-interaction latency at the cost of an extra network request on page load.
Opens the stream connection before `init()` completes. Trades a small amount of bandwidth for faster time-to-first-frame.
Keeps the local SDK state alive during user inactivity. With the default (`false`), the SDK tears down its local Daily call object after 5 minutes idle and emits the [`idle`](/web-sdk/reference/events#idle) event. The backend session terminates separately via `user_absent_timeout`.
Override the API host. Rarely needed — when set, expose both `${apiHost}/api/v1` and `${apiHost}/_sdk/v0` (see [Under the hood](/web-sdk/integration-guide#under-the-hood)).