Skip to content

Concepts

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:

  • By id: avatar_id values look like avt_441ad4d95906438490a85b79. This is the string you pass to POST /v1/sessions.
  • By handle: (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:

  • For public catalog avatars (Dr. Alman, Jesus, Vi, Kai, etc.): resolve by handle via 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.
  • For avatars you create in the Developer Console: they’re automatically yours — your sk_ can mint them once you flip the API-expose toggle.
  • Same character, different ids across accounts. Dr. Alman on Enlightn’s integration and Dr. Alman on another customer’s integration are the exact same character — same visual, same voice persona, same brain — but each account has its own avt_... id. Think of avt_ as a per-account license key for the same character, not a different character.
  • Visibility is reversible. If a creator toggles their avatar’s API-Visible off in Studio (or an avatar is unpublished), the next mint on your 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:

  • Use a voice already registered on your account, or
  • Register your own with 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 call
  • livekit_url + participant_token + room_name: what you hand to the client so it can join the LiveKit room and render the stream
  • agent_identity: the LiveKit participant identity the agent will join as

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