Zeli AvatarDeveloper docs

API reference / LiveKit

LiveKit

Four routes put a Zeli Avatar into a LiveKit room you own and take it out again. Your agent keeps speaking through livekit-agents; the avatar joins as its own participant and turns that speech into a lip synced face. For the walkthrough, start with the LiveKit guide.

MethodPathPurposeAuth
POST/livekit/joinJoin a room and publish the avatar into itfull
POST/livekit/leaveLeave a room and free the avatar serverfull
GET/livekit/statusIs the avatar server free, busy or drainingfull
GET/livekit/readinessCan this avatar server join LiveKit rooms at allfull

Every route takes a full API key (zsk_live_...) in X-Api-Key. Session tokens (zsk_temp_...) are refused with 401 insufficient_scope: a join spends a GPU and carries a room token, so it is made from your server, never a browser. These routes reach a running avatar avatar server, like every /api/* route.

Join a room

POST/livekit/join

Joins your LiveKit room with the token you minted, publishes the avatar's video and audio, and starts listening for your agent's speech. Answers once the avatar is in the room, usually in about 20 seconds. One avatar server serves one room at a time, so a join while the avatar server is busy is refused rather than allowed to cut somebody else's call.

livekit_urlstringRequired

Your LiveKit server URL, for example wss://your-project.livekit.cloud.

livekit_tokenstringRequired

A room token for the avatar's identity, signed with your LiveKit API secret: kind agent, roomJoin for this room, and the attribute lk.publish_on_behalf set to your agent's identity. Used exactly as given. The avatar server never holds your secret.

room_namestringRequired

The room to join.

identitystringOptional

The identity your token joins as. Default zeli-avatar. The avatar server checks the token against it and refuses a mismatch, because a face your agent is not talking to would sit in the room silent.

engine_identitystringOptional

Your agent's participant identity: whose audio and tone messages to accept. Empty means the first agent participant in the room.

avatarstringOptional

The avatar to show. Falls back to your saved avatar, then the avatar server's active one.

expected_minutesnumberOptional

How long you expect the call to run. An avatar server that will shut down sooner answers 503 box_draining now instead of dropping the call later. Answered 400 livekit_bad_request when it is not a JSON number (a string or a boolean), is not greater than zero, is more than the avatar server's max_minutes ceiling (at most 120 by default), or is more than the max_minutes you sent (the session would end before the call).

max_minutesnumberOptional

This session's maximum length. Lowered to the avatar server's ceiling (120 minutes by default).

idle_minutesnumberOptional

This session's idle timeout. Lowered to the avatar server's ceiling (20 minutes by default).

usageobjectOptional

Optional ids for this meeting's usage record, for example {"meeting_id": "your-call-id"}. Kept on the record. The meeting is always billed to the account that owns the API key.

{
  "ok": true,
  "identity": "zeli-avatar",
  "room": "support-room-1",
  "avatar": "your-avatar-id",
  "silence": "your-avatar-id__silence",
  "idle_stand_in": false,
  "limits": {
    "max_minutes": 60.0,
    "idle_minutes": 5.0,
    "expected_minutes": 45.0,
    "clamped": []
  },
  "session_id": "0192f3a4b5c6d9f1e2a3b4c5d"
}

identity is the identity the avatar server actually joined as, read back from your token. limits are the limits this session runs under, and clamped names any you asked for that were lowered. session_id is the id of this meeting's usage record: keep it with your call.

StatusCodeMeaning
400livekit_bad_requestA required field is missing, a limit is not a number greater than zero, or expected_minutes is over the avatar server's ceiling or over your max_minutes
409livekit_session_in_useThe avatar server is already serving a session
409livekit_join_abortedA leave for this room arrived while the join was in flight
502livekit_join_failedThe avatar server could not connect to the room with that URL and token
502livekit_identity_mismatchThe token joins as a different identity than identity
503box_drainingThe avatar server will shut down before the call would end
503livekit_unavailableThis avatar server cannot join LiveKit rooms

Leave a room

POST/livekit/leave

Leaves the room and frees the box. Idempotent: leaving twice answers left: false the second time rather than an error.

room_namestringOptional

The room to leave. Always send it. Named, the leave only ends a session in that room, and cancels a join for it that is still in flight. Another room answers 409 livekit_not_in_room and nothing is touched.

{ "ok": true, "left": true, "room": "support-room-1", "session_id": "0192f3a4b5c6d9f1e2a3b4c5d" }

Only a key from the account that joined the room, or the avatar server operator, may end it. Anybody else gets 403 livekit_not_owner, and the room stays up. See "Who may end a room" below for the other routes this covers.

Who may end a room

While a LiveKit room is live on the avatar server, only a key from the account that joined it, or the avatar server operator, may end it. That holds on every route that would end it:

RouteWhat it would doAnybody else gets
POST /livekit/leaveLeave the room403 livekit_not_owner
POST /disconnectEnd the live session403 livekit_not_owner
POST /connectTake the avatar server over for a browser session, ending the room403 livekit_not_owner
POST /api/avatar/deleteStop the stream before deleting an avatar403 livekit_not_owner

Who may drive a room. The same rule covers the routes that would act in the room without ending it:

RouteWhat it would doAnybody else gets
POST /api/chatSpeak a reply into the room403 livekit_not_owner, nothing is said
GET /api/voice/wsTurn a microphone into turns in the room, cut its speech, or end it through a persona end callClose code 4003 with error_code: "livekit_not_owner" on the error frame

A microphone opened on a browser session and still open when a room another account joined goes live cannot drive that room either: its next turn closes it the same way.

The room stays up on every refusal. If the avatar server cannot look your key up at that moment it answers 503 livekit_owner_unverified instead and ends nothing (on the microphone socket: close code 4006 with that error_code): retry in a moment. A browser session with no LiveKit room is not affected.

Box status

GET/livekit/status

Whether this avatar server can take a call now, and for how long. Always 200: the state is the answer.

avatarstringOptional

Also report whether this avatar is already loaded (a warm face joins faster).

{
  "ok": true,
  "state": "free",
  "minutes_remaining": null,
  "shutdown_minutes_remaining": null,
  "drain_threshold_minutes": 55.0,
  "idle_shutdown": false,
  "drain": false,
  "limits": { "max_minutes_ceiling": 120.0, "idle_minutes_ceiling": 20.0 },
  "room": null,
  "session_id": null,
  "avatar": "your-avatar-id",
  "avatar_warmed": true,
  "box_id": "i-0abc123def4567890",
  "build": "14b454e"
}

state is free, busy or draining. room and session_id are shown only to the account that joined the room.

LiveKit readiness

GET/livekit/readiness

Whether this avatar server can join LiveKit rooms at all, and on which releases. Always 200.

{
  "ok": true,
  "installed": true,
  "problem": null,
  "versions": {
    "livekit": "1.1.8",
    "livekit-agents": "1.5.13",
    "livekit-api": "1.1.0",
    "livekit-protocol": "1.1.15"
  },
  "identity": "zeli-avatar",
  "topic": "lk.audio_stream"
}

installed: false comes with a problem saying what is missing. identity is the default identity a join expects, and topic the data stream topic the avatar server listens on for your agent's audio.

Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source