Avatar
The character being rendered — a lip-synced likeness identified by an
avatar_id (e.g. avt_...) or by (owner, avatarname) handle.
Five ideas cover almost everything in this API.
Avatar
The character being rendered — a lip-synced likeness identified by an
avatar_id (e.g. avt_...) or by (owner, avatarname) handle.
Voice
The speech synthesis identity used when 2wai generates audio, identified
by a voice_id.
Session
A live, running instance of an avatar — the thing your server creates, drives, and eventually ends.
Mode
What the session produces. Today, video (lip-synced video + audio).
Transport 0 (LiveKit)
The wire the session streams over. LiveKit (WebRTC) is the only live transport; MSE-over-WebSocket is retired.
An avatar is the visual identity a session renders. Two ways to refer to one:
avatar_id values look like avt_441ad4d95906438490a85b79. This is the string you pass to POST /v1/sessions.(owner, avatarname) — e.g. (brian-alman, brian) for Dr. Alman. Resolve via GET /v1/avatars/{owner}/{name} (catalog hydrate) for display name, idle video, poster, and optional greeting. See Catalog hydrate for the shape; today it’s for UI metadata, not for mint (see the tenancy section below).Public avatars appear on the 2wai.ai catalog and resolve via catalog hydrate on any sk_. Avatars created in the Developer Console (your custom ones) are reachable through your sk_ once you opt them into the developer API via Studio’s API-expose toggle on the avatar’s row.
An avatar and a voice are independent: the same avatar can speak in different voices across sessions.
The single most common early-integration gotcha. An avatar_id is scoped to the account that owns the row — the gateway’s POST /v1/sessions lookup is WHERE id = <avatar_id> AND tenant_id = <authenticating sk's tenant>. So an avt_... id that appears in a sample snippet or a reference client will not mint on your sk_ — it belongs to that sample’s tenant, not yours. Expect 404 no_such_avatar.
What this means practically:
GET /v1/avatars/{owner}/{name} (catalog hydrate) and pass the returned avatar_id straight to POST /v1/sessions. If your account doesn’t have a local row for that avatar yet, the gateway auto-mirrors it from gen-2wai on first mint — zero per-avatar admin provisioning. Deep-link flows like myapp.com/chat/<owner>/<name> → getAvatar(owner, name) → mint → connect work end-to-end.sk_ can mint them once you flip the API-expose toggle.avt_... id. Think of avt_ as a per-account license key for the same character, not a different character.sk_ returns no_such_avatar — the mirrored row stays locally but gen-2wai is re-checked on every LiveKit mint, so Studio-side authorization changes take effect immediately.If you see no_such_avatar on first mint: either (a) the avatar’s handle is wrong — double-check against the catalog, or (b) the avatar’s API-Visible is off / unpublished / not on LiveKit. Hitting GET /v1/avatars/{owner}/{name} is the fast diagnostic: 200 means it’s mintable, 404 means one of the gates is closed.
A voice is the synthesis identity 2wai uses to generate speech from text when you call /speak. You can:
POST /v1/voices, cloned instantly from a short reference clip (see Voices).If you’d rather bring your own TTS, skip voices entirely and push synthesized audio with POST /v1/sessions/{id}/audio — see Bring your own TTS. The voice_id field is then irrelevant for that session.
A session is one running instance of an avatar: the unit you create, drive, and end. Creating a session (POST /v1/sessions) starts the render pipeline and returns:
session_id: how your server refers to it on every subsequent calllivekit_url + participant_token + room_name: what you hand to the client so it can join the LiveKit room and render the streamagent_identity: the LiveKit participant identity the agent will join asA session stays alive until you end it (DELETE /v1/sessions/{id}), it idles out, or the LiveKit connection fails for long enough. See Sessions for the full lifecycle.
Each session counts against your account’s concurrency limit — see Limits & concurrency.
mode on POST /v1/sessions selects what the session produces. Today:
video — lip-synced video + audio tracks on the LiveKit room. The default and the shape every quickstart and SDK targets.Future modes may be added (audio-only, call-mode); check API Reference for the current list.
Every session streams over Transport 0 — a LiveKit room, media delivered as subscribed WebRTC tracks (audio + video). This has been the sole live transport since 2026-08-22 per ADR-049 on the engineering wiki.
Historical note: the gateway’s older MSE-over-WebSocket path (transport: "mse-ws", mse_url, fMP4 fragments appended to a SourceBuffer) is retired on the production gateway and silently fails. Don’t model new integrations on it. Pass transport: "livekit" (or omit — it’s the default on current gateway versions) on POST /v1/sessions.
A typical session request:
{ "avatar_id": "avt_441ad4d95906438490a85b79", "mode": "video", "voice_id": "vc_e72e888d566aca3a4e63fbe8", "transport": "livekit"}…starts a video session of a specific avatar, in a specific voice, over Transport 0. Everything else in this API is about creating that session, driving what it says, and eventually ending it.
Next: Sessions for the lifecycle in depth, or Streaming into your app for the client side.