LiveKit Agents plugin
Bring your own LiveKit voice agent; Atmee renders a talking-head avatar from a single portrait into your room and bills per minute.
What it is
You already run a voice agent on LiveKit Agents with your own speech recognition, model and voice. The Atmee plugin adds a face: a talking-head avatar, rendered from one portrait, joins your LiveKit room as its own participant and speaks your agent's audio with lips in sync. Atmee never sees your LiveKit secret and never hosts your conversation; it only renders, and bills by the minute while it does. If you would rather have Atmee run the whole conversation and only join a room from your page, use the Session API instead; Choose your integration compares the two.
from livekit.plugins import atmee
avatar_id = await atmee.AtmeeAPI().create_avatar(name="Val", image="portrait.jpg")
avatar = atmee.AvatarSession(avatar_id=avatar_id) # ATMEE_API_KEY in the environment
await avatar.start(session, room=ctx.room)
await session.start(agent=..., room=ctx.room) # the agent's TTS drives the avatar's videoInstallation
pip install livekit-plugins-atmeePython 3.10 or newer, livekit-agents 1.6.8 or newer.
Prerequisites
- An Atmee API key (
sk_atmee_…) with thesessionsscope, inATMEE_API_KEY. Keep it on your agent's side; it never goes to a browser. See API keys. - Your own LiveKit project (LiveKit Cloud or self-hosted):
LIVEKIT_URL,LIVEKIT_API_KEY,LIVEKIT_API_SECRET, the same variables your agent already uses. The plugin mints the avatar's room token locally with your secret and hands only that token to Atmee.
Usage
1. Create an avatar from a portrait
Once, from a script or your backend:
async with atmee.AtmeeAPI() as api:
avatar_id = await api.create_avatar(name="Val", image="portrait.jpg") # path, bytes, or https URLA portrait-only avatar is render-only (kind: render_only): it has no voice or knowledge of
its own and is ready immediately, because there is nothing to build. It can only be used through
avatar sessions, not on atmee.ai or through POST /v1/session. Any avatar of your account that has
a portrait works too, including ones created in the Studio or with a voice through
POST /v1/avatars.
2. Render it in your agent
async def entrypoint(ctx: agents.JobContext):
await ctx.connect()
session = AgentSession(stt=..., llm=..., tts=..., vad=...)
avatar = atmee.AvatarSession(avatar_id=os.environ["ATMEE_AVATAR_ID"])
await avatar.start(session, room=ctx.room) # before session.start
await session.start(agent=MyAgent(), room=ctx.room)start() returns as soon as Atmee's rendering worker acknowledged the start, about a second. The
avatar's video and audio tracks appear in the room a few seconds later, published by the
participant atmee-avatar-agent. Your agent publishes no audio of its own; the avatar speaks
for it.
Options on AvatarSession:
| Argument | Default | Meaning |
|---|---|---|
avatar_id | required | The Atmee avatar to render. |
api_key, api_url | ATMEE_API_KEY, ATMEE_API_URL | Credentials and API base. |
avatar_participant_identity | atmee-avatar-agent | Identity the avatar joins with; set it when one room hosts several avatars. |
max_duration_seconds | 3600 | Hard ceiling of the session; also the most it can bill if your agent dies without a trace. |
wait_for | "initializing" | "avatar_joined" makes start() block until the avatar is in the room. |
metadata | None | Free-form JSON stored with the session on the Atmee side. |
avatar.session_id and avatar.session_info hold the Atmee session after start();
avatar.on("avatar_disconnected", ...) fires if the avatar participant leaves while your agent is
still running.
Lifecycle and billing
Billing starts when the avatar sees your agent in the room and runs by the minute at the render rate, from your account's credits. The render rate is lower than a hosted session's, since Atmee only renders. It stops when
- your agent leaves the room or the room closes (the rendering worker notices and reports the end itself, so a crashed agent stops being billed within seconds),
aclose()runs, registered automatically as a job shutdown callback: it ends the session and removes the avatar participant from your room,- the rendering worker itself dies or loses contact: it proves it is alive once a minute, and a session silent for a few minutes is closed and billed only up to the last proof,
- or the session reaches
max_duration_seconds.
Each session is a normal API-key session for billing purposes: your key's spend cap, concurrency
limit and billingMode apply (see Reference).
Errors
All API failures raise atmee.AtmeeException with status_code, code and message. Two
subclasses are worth handling: AtmeeNoCapacityError (every rendering worker is busy; check
retry_after and try again later) and AtmeeAvatarNotReadyError (the avatar has no portrait
yet). The codes:
| Status | code | Meaning |
|---|---|---|
400 | invalid_livekit_url | livekitUrl is not a ws(s):// server URL. |
400 | invalid_livekit_token | Not a LiveKit access token, no room grant, or expired. |
400 | missing_publish_on_behalf | The token names no agent to render for (lk.publish_on_behalf). |
400 | agent_identity_mismatch | agentIdentity differs from the token's lk.publish_on_behalf. |
402 | insufficient_credits … | Out of credits, spend cap reached, or too many sessions for this key. |
403 | avatar_not_allowed | The avatar is not on this key's allow-list. |
409 | avatar_not_renderable | The avatar has no reference image to animate. |
502 | avatar_start_failed | The worker refused the render before joining (for example the portrait has no usable face). Nothing is billed. |
503 | no_capacity | Every rendering worker is busy; retry after Retry-After seconds. |
503 | avatar_sessions_disabled | This deployment has no rendering backend. |
A token your LiveKit server rejects never joins; the session then fails as AVATAR_DID_NOT_JOIN
after the join timeout and is not billed.
Endpoints
The plugin is a thin client over three endpoints. Use them directly from any language; all are
authenticated with X-Api-Key like the rest of the API.
POST /v1/avatars/{avatarId}/avatar_sessions
Starts an avatar session. Responds 202 once the worker acknowledged the start; add
?waitFor=avatar_joined to block until the avatar participant is in your room.
| Field | Type | Notes |
|---|---|---|
livekitUrl | string, required | Your LiveKit server URL (wss://<project>.livekit.cloud or a self-hosted ws(s):// address). |
livekitToken | string, required | Access token for the avatar participant, minted with your LiveKit key and secret: identity = the avatar's participant identity, kind: agent, a roomJoin grant for your room, attribute lk.publish_on_behalf = your agent's identity. Read, never verified. |
agentIdentity | string | Optional cross-check; must equal the token's lk.publish_on_behalf. |
maxDurationSeconds | integer 60–14400 | Hard ceiling (default 3600). Capped by your key's limits. |
metadata | object, ≤50 keys | Stored with the session. |
Response 202: sessionId, status (initializing or avatar_joined),
avatarParticipantIdentity, agentIdentity, roomName, maxDurationSeconds, billingMode.
GET /v1/avatar_sessions/{id}
The session's durable state: status (pending, active, completed, failed),
avatarJoinedAt, agentJoinedAt (billing starts here), endedAt. 403 forbidden for another
account's session, 404 not_found.
POST /v1/avatar_sessions/{id}/end
Stops billing now. Idempotent: ending a session that already ended answers 200 with
alreadyEnded: true, so you can call it from every teardown path. The worker's own report when
your agent leaves is the primary end signal; this call is the fallback, and the way to stop paying
for an avatar you are done with while your room stays open. It does not remove the avatar from
your room; remove the participant yourself (the plugin does so on close).
How it works
AvatarSession.start()mints a LiveKit access token for the avatar participant with your LiveKit credentials: identityatmee-avatar-agent,kind: agent, aroomJoingrant for your room, and the attributelk.publish_on_behalfset to your agent's identity. Atmee receives only that token, never your secret.- It calls
POST /v1/avatars/{avatarId}/avatar_sessionswith your LiveKit URL and the token. Atmee reserves a session, starts a rendering worker, and answers as soon as the worker acknowledged the start. - The worker joins your room with the token, waits for your agent, receives its audio over the
LiveKit data stream (
lk.audio_stream, 16 kHz PCM, the standardDataStreamAudioOutput), and publishes lip-synced video and audio on behalf of your agent. - When your agent leaves or the room closes, the worker notices from inside the room and reports
the end to Atmee, which stops billing.
aclose()removes the avatar participant and ends the session explicitly as well.
Examples
The plugin repository ships two examples: examples/agent.py, a Deepgram + OpenAI + Silero agent
with an Atmee avatar (run with python examples/agent.py dev and open the LiveKit Agents
Playground), and examples/create_avatar.py, which creates an avatar from a portrait and prints
its id.
