Skip to main content
This guide walks the direct API path end to end. If you build on LiveKit Agents or Pipecat, the plugins do all of this for you.

Before you start

You need four things:
  • A FaceMode account - sign-up is waitlist-gated during the private beta; join at console.facemode.io/signup. Approved testers get 3,600 free credits (60 minutes).
  • A FaceMode API key (fm_...) from the dashboard. See Authentication.
  • An avatar ID for the avatar that should speak.
  • A room you own on LiveKit or Daily, plus a token that lets FaceMode join and publish. For LiveKit, mint a token with room_join, can_publish, and can_subscribe grants on the room name.
Beta limits: sessions auto-end after 10 minutes, one concurrent session per account, and a session needs at least 60 credits of remaining balance to start. Credits are metered per second (1 credit = 1 second of streamed avatar video) and never expire. Credit purchases are disabled during the beta - top-ups for extra credits open at general availability.
Set these as environment variables on your backend:
Every credential in this guide stays on your server. The browser only ever receives the room URL and its own viewer token - never the API key, the session wsToken, or the publish token.
1

Create a session

Call POST /api/sessions with the avatar and the room the avatar should publish into.
Request bodyA 201 response returns the session plus ready-to-use ingestion credentials:
string
required
The session ID. You need it for start, reconnect, status polling, and ending the session.
boolean
true when ingestion.url and ingestion.wsToken are usable immediately - no polling required.
string
The ingestion WebSocket URL for this session.
string
A one-time, 15-minute JWT used as the WebSocket subprotocol credential.
object
Optional extra headers to forward on the WebSocket handshake when present.
string
The room URL the avatar will publish to, echoed back at the top level.
string
The publisher room token you supplied, echoed back at the top level.
string
Avatar session lifecycle: PENDING, ASSIGNING, ASSIGNED, ACTIVE, ENDING, ENDED, or FAILED.
Unknown or legacy fields are rejected. waitForIngestion returns WAIT_FOR_INGESTION_REMOVED, and byolr, livekitUrl, or livekitToken return ROOM_LEGACY_FIELDS - always use the room object.
2

Connect the ingestion WebSocket

Open a WebSocket to ingestion.url and authenticate with the Sec-WebSocket-Protocol subprotocol facemode.<wsToken>:
Clients that cannot set a subprotocol may instead append the same JWT as a query parameter: wss://.../ws/<session_id>?token=<wsToken>.
wsToken is single-use and expires 15 minutes after issue. Once a handshake consumes it, never reuse it - call POST /api/sessions/:id/reconnect to mint a fresh one. Only the facemode. prefix is accepted.
3

Send start and wait for started

The first message on the socket must be start. It negotiates the audio format for the whole session.
FaceMode replies:
Allow up to ~240 seconds for the handshake plus start exchange on cold starts - a fresh session may still be starting up when you connect. The official plugins use a 240-second open budget.
4

Stream audio

With pcm_s16le or pcm_f32le, send each audio chunk as a raw binary WebSocket frame. With base64_json, send JSON audio messages instead:
Utterance boundaries. Two styles are supported - pick one per session:
  • Inferred (recommended): send start, then PCM frames. FaceMode detects the end of each utterance after the silence gap (utterance_silence_ms, default 800 ms). You get audio_ready when speech starts rendering and utterance_ended after the gap.
  • Explicit: wrap each utterance in start_utterance{seq} … end_utterance{seq}, and use cancel_utterance{seq} for barge-in. end_utterance is acknowledged by utterance_ended{seq, cycles} once the session drains.
Sending any explicit utterance control permanently latches the session into explicit mode. Never mix both styles on one connection.
Sequence numbers (seq) are non-negative integers that must not move backwards. Control frames carry {"type": "...", "seq": n}.
5

Join the room from your frontend

Pass the room URL to your frontend along with a separate viewer token you mint for the end user (subscribe-only for LiveKit). The roomToken returned by the API is the publish credential you supplied for FaceMode - do not hand it to browsers.
Browser
While no one is speaking, viewers see the idle animation; when your audio arrives the avatar crossfades into lip-synced speech.
6

End the session

  1. Send end_session on the WebSocket and wait for the ended acknowledgement:
  2. Call POST /api/sessions/:id/end:
    200 means the session is finalized. 202 ("pending": true) means session cleanup is still in progress - keep polling.
  3. Poll GET /api/sessions/:id until session.status is ENDED:
The ended frame is a protocol acknowledgement, not confirmation that backend cleanup finished. Treat the session as done only when the REST status reaches ENDED.

Minimal full-session example

A complete session in one file: create, connect, start, stream one PCM file, end, and poll until terminal.

Timing budgets

These are guidance values, not hard limits:

Reconnecting

If the WebSocket drops mid-session, do not reuse the old token. Call POST /api/sessions/:id/reconnect (optionally with a replacement room) to mint a fresh wsToken, then reconnect with facemode.<newToken> and resend start.

Errors

REST errors use a consistent envelope:
Common codes: ROOM_INVALID, ROOM_LEGACY_FIELDS, REQUEST_FIELD_UNKNOWN, WAIT_FOR_INGESTION_REMOVED, INPUT_PROVIDER_INVALID, AVATAR_NOT_FOUND, SESSION_NOT_FOUND, SESSION_ALREADY_ENDED. WebSocket errors arrive as error frames:
A fatal: true error ends the connection; reconnect with a fresh wsToken.