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 boxfull
GET/livekit/statusIs the box free, busy or drainingfull
GET/livekit/readinessCan this box 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 box, 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 box serves one room at a time, so a join while the box 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 box never holds your secret.

room_namestringRequired

The room to join.

identitystringOptional

The identity your token joins as. Default zeli-avatar. The box 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 box's active one.

expected_minutesnumberOptional

How long you expect the call to run. A box 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 box'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 box's ceiling (120 minutes by default).

idle_minutesnumberOptional

This session's idle timeout. Lowered to the box'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 box 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 box's ceiling or over your max_minutes
409livekit_session_in_useThe box is already serving a session
409livekit_join_abortedA leave for this room arrived while the join was in flight
502livekit_join_failedThe box could not connect to the room with that URL and token
502livekit_identity_mismatchThe token joins as a different identity than identity
503box_drainingThe box will shut down before the call would end
503livekit_unavailableThis box 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 box 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 box, only a key from the account that joined it, or the box 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 box 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 box 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 box can take a call now, and for how long. Always 200: the state is the answer.

Every box at once, with no box asked

This route answers for the one box it reaches. Each box also writes its state, room, build and warm avatars into the box registry every 15 seconds, and portal admins read every box from that registry with GET /auth/admin/fleet, which is served by the always on service and asks no box anything. It answers {generated_at, boxes: [{box_id, public_url, region, state, room, build, heartbeat_at, heartbeat_age_seconds, fresh, capacity, avatars_warm, drained, leased}]}, newest heartbeat first, where fresh means a heartbeat within the last 45 seconds and build is {known, short_commit}. It is admin only and takes a portal session; it is what the admin dashboard's Fleet panel and the GPU page's build line read. The box keeps this route, /livekit/readiness and /api/stats for the questions only a running box can 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 box 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 box listens on for your agent's audio.

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