Skip to main content
A FaceMode session is one avatar streaming into one room. It has two halves that you manage together: a control half on the REST API (create, poll, reconnect, end) and a media half on a per-session ingestion WebSocket (negotiate once, stream audio, receive events).

Two kinds of state

Every session response exposes two fields:
  • status - the session’s own state. It is ACTIVE from creation until the session is fully cleaned up, when it becomes ENDED.
  • workerStatus - the state of the avatar session provisioned for it.
ENDED and FAILED are terminal. Once workerStatus reaches either, the ingestion socket is gone for good - create a new session instead of retrying.

Sequence of a healthy session

Step by step

1

Create the session

POST /api/sessions with avatarId, a room object, and an optional inputProvider. A healthy create returns 201 and usually includes ingestion.ready: true with url and wsToken immediately - session creation is connect-first.
2

Wait for ingestion readiness

If ingestion.ready is false, poll GET /api/sessions/:id with bounded backoff until ingestion.ready is true. A practical budget is about 240 seconds total for cold starts, starting near 250 ms and capping the retry interval around 2 seconds. Abort early if workerStatus becomes FAILED or ENDED.
3

Open the ingestion WebSocket

Connect to ingestion.url with the subprotocol facemode.<wsToken>. The token is a one-time, 15-minute JWT: a consumed token is never accepted again, so each socket needs its own token. Clients that cannot set subprotocols may pass the same token as a ?token= query parameter. If the response includes ingestion.headers, forward them on the upgrade request.Budget up to about 240 seconds for the handshake so a cold-starting session has time to accept the connection, and disable permessage-deflate on the socket. The official plugins use a 240-second open budget.
4

Negotiate with start

The first application message on the socket must be start. It pins the audio format for the life of the session.
The session answers started:
5

Stream utterances

Send audio as binary PCM frames, or as audio JSON messages when you negotiated base64_json. See Utterance flow below for inferred vs explicit mode.
6

Keep the connection healthy

After started, send an application ping roughly every 15 seconds and expect pong back. This is a transport heartbeat only - it does not count as session activity. A POST /api/sessions/:id/heartbeat call is also available to refresh the session’s activity timestamp from the control plane.
7

End the session

Send end_session, wait for ended, then close the socket and call POST /api/sessions/:id/end. See Ending and cleanup for why the ended message alone is not enough.

Utterance flow

The protocol supports two utterance modes. Pick one per session - sending any explicit utterance control permanently latches the session into explicit mode.
Bracket each utterance yourself: start_utterance { seq }, audio frames, then end_utterance { seq }. FaceMode replies utterance_ended { seq, cycles } when the utterance has drained - seq matches your end_utterance and cycles counts the inference cycles consumed. Use cancel_utterance { seq } to abort an in-flight utterance, for example on barge-in. Keep seq monotonically increasing across the session, including across reconnects.

Message reference

Client to server: Server to client:

Error codes

Errors carry fatal: true when the socket is about to close; treat fatal errors as end-of-session.

Reconnecting a dropped transport

If the WebSocket drops mid-session, do not create a new session - call POST /api/sessions/:id/reconnect instead:
  • The response returns a fresh ingestion block with a new one-time wsToken. Reuse the same reconnect budget you used for the first connect.
  • room is optional and only needed if you are rotating room credentials. Its type must match the session’s original room type, and inputProvider (if sent) must match the value used at creation.
  • Open a new socket, resend the identical start message, and wait for started before resuming audio. Keep seq numbers monotonic across the reconnect.
  • Bound your attempts (two or three retries with short backoff is typical) and end the session if reconnects are exhausted.
A dropped socket does not end the session. FaceMode keeps the session warm briefly so a reconnect can reattach cleanly - but audio sent during the gap is lost, so pause upstream until started arrives on the new socket.

Ending and cleanup

Ending has two layers, and conflating them causes leaked sessions:
  1. Protocol layer - end_session on the socket is answered by ended. This is only an acknowledgement that FaceMode received your request. It is not proof that backend cleanup finished.
  2. Control layer - POST /api/sessions/:id/end finalizes the session. It returns 200 with pending: false when cleanup completed inline, or 202 with pending: true while session cleanup is still in flight. A 400 with SESSION_ALREADY_ENDED means a previous request already finalized it - treat that as success, not failure.
After a 202, poll GET /api/sessions/:id every half-second or so until status is ENDED (or workerStatus reaches ENDED/FAILED). A practical budget is about 45 seconds. FaceMode can also end the session on its own: it sends session_ending with a reason, then closes the socket with code 1000. Sessions with no audio activity for an extended idle period are ended automatically, so explicit cleanup on your side is always the safer path. The reason is available as workerAssignment.terminationReason on status reads. A session in ENDING no longer blocks a new POST /api/sessions - the concurrency slot is freed when termination is requested.
Always end sessions you no longer need. An abandoned session keeps its avatar - and its room participant - alive until the idle policy reclaims it.