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.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /livekit/join | Join a room and publish the avatar into it | full |
| POST | /livekit/leave | Leave a room and free the avatar server | full |
| GET | /livekit/status | Is the avatar server free, busy or draining | full |
| GET | /livekit/readiness | Can this avatar server join LiveKit rooms at all | full |
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
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.
Your LiveKit server URL, for example wss://your-project.livekit.cloud.
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.
The room to join.
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.
Your agent's participant identity: whose audio and tone messages to accept. Empty means the first agent participant in the room.
The avatar to show. Falls back to your saved avatar, then the avatar server's active one.
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).
This session's maximum length. Lowered to the avatar server's ceiling (120 minutes by default).
This session's idle timeout. Lowered to the avatar server's ceiling (20 minutes by default).
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.
| Status | Code | Meaning |
|---|---|---|
| 400 | livekit_bad_request | A 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 |
| 409 | livekit_session_in_use | The avatar server is already serving a session |
| 409 | livekit_join_aborted | A leave for this room arrived while the join was in flight |
| 502 | livekit_join_failed | The avatar server could not connect to the room with that URL and token |
| 502 | livekit_identity_mismatch | The token joins as a different identity than identity |
| 503 | box_draining | The avatar server will shut down before the call would end |
| 503 | livekit_unavailable | This avatar server cannot join LiveKit rooms |
Leave a room
Leaves the room and frees the box. Idempotent: leaving twice answers
left: false the second time rather than an error.
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:
| Route | What it would do | Anybody else gets |
|---|---|---|
POST /livekit/leave | Leave the room | 403 livekit_not_owner |
POST /disconnect | End the live session | 403 livekit_not_owner |
POST /connect | Take the avatar server over for a browser session, ending the room | 403 livekit_not_owner |
POST /api/avatar/delete | Stop the stream before deleting an avatar | 403 livekit_not_owner |
Who may drive a room. The same rule covers the routes that would act in the room without ending it:
| Route | What it would do | Anybody else gets |
|---|---|---|
POST /api/chat | Speak a reply into the room | 403 livekit_not_owner, nothing is said |
GET /api/voice/ws | Turn a microphone into turns in the room, cut its speech, or end it through a persona end call | Close 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
Whether this avatar server can take a call now, and for how long. Always 200: the state
is the answer.
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
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.