# Render an avatar into your own LiveKit room (API-key authenticated)

URL: https://www.atmee.ai/docs/v2/api/reference/avatar-sessions/startAvatarSession

`POST /v1/avatar_sessions`

Starts an **avatar session**: the avatar is rendered into a LiveKit room
that YOU own, on behalf of YOUR agent — the bring-your-own-agent path the
`livekit-plugins-atmee` plugin uses. Nothing here creates a room or a
conversation: you mint a LiveKit access token for the avatar participant
with your own project's API secret (grant `roomJoin` for your room, set the
participant attribute `lk.publish_on_behalf` to your agent's identity, and
`kind: agent`) and pass it here. The rendering worker joins your room with
that token, waits for your agent, receives its audio over the LiveKit data
stream (`lk.audio_stream`, 16 kHz PCM — livekit-agents'
`DataStreamAudioOutput`) and publishes synced video and audio.

Responds `202` as soon as the worker acknowledged the start (`status:
initializing`), typically within a second; the avatar's tracks appear in
your room a few seconds later. Pass `?waitFor=avatar_joined` to block
until the worker is in the room instead. Billing runs by the minute while
the render lasts and stops when the worker reports the end (your agent
left, the room closed, you removed the avatar participant), when you call
`POST /v1/avatar_sessions/{id}/end`, or at `maxDurationSeconds`.

The token is read, not verified — your secret never leaves you. A token
your LiveKit server rejects simply never joins and the session fails as
`AVATAR_DID_NOT_JOIN` after the join timeout.

**Atmee-hosted room** (no LiveKit project of your own, e.g. a Pipecat
bot on any transport): omit both `livekitUrl` and `livekitToken`. The
service then creates a room on Atmee's LiveKit project, the avatar joins
it as `atmee-avatar-agent` publishing on behalf of `agentIdentity`
(default `agent`), and the response carries `livekitUrl` and
`agentToken` for your agent to join that room with. The protocol inside
the room is the same: stream your agent's audio to the avatar
(`lk.audio_stream`), subscribe to the avatar's video and audio tracks.
The room is deleted when the session ends. Requires the `sessions`
scope; the avatar must be on the key's allow-list.

**v2 avatars** (invite-only) render on a separate GPU pool: the avatar's
own configuration decides, there is nothing to set on the request. That
pool scales to zero, so the first v2 session after a quiet period can
take a few minutes to start: the service waits up to 4 minutes for the
pool to start the render and up to 6 for the avatar to join. Use
`?waitFor=avatar_joined` with a client timeout of at least 7 minutes, or
retry on `avatar_warming`.

## Parameters

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `waitFor` | query | no | `initializing` (default) returns once the worker acknowledged the start; `avatar_joined` returns once the avatar participant is in your room. |

## Request body

- `application/json`: `AvatarSessionStartRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `avatarId` | string | yes | The avatar to render. Must be on the key's allow-list. |
| `livekitUrl` | string | no | Your LiveKit server URL (`wss://<project>.livekit.cloud`, or a self-hosted `ws(s)://` address). Omit together with `livekitToken` for an Atmee-hosted room. |
| `livekitToken` | string | no | LiveKit access token for the avatar participant, minted with YOUR project's API key and secret: `identity` = the avatar's participant identity (the plugin uses `atmee-avatar-agent`), `kind: agent`, a `roomJoin` grant for your room, and the attribute `lk.publish_on_behalf` = your agent's identity. Read, never verified, by this service. Omit together with `livekitUrl` for an Atmee-hosted room. |
| `agentIdentity` | string | no | With your own room: optional cross-check, must equal the token's `lk.publish_on_behalf`. In an Atmee-hosted room: your agent's participant identity (default `agent`; `atmee-avatar-agent` is reserved for the avatar). |
| `maxDurationSeconds` | integer | no | Hard ceiling of the session, in seconds (default 3600). Also the most a session can bill if your agent dies without the worker ever reporting. Capped server-side by the key's limits. |
| `metadata` | object | no | Optional free-form metadata to attach to the session (stored as JSONB). |

## Responses

| Status | Description | Schema |
| --- | --- | --- |
| `202` | The avatar is starting (or has joined, with `waitFor=avatar_joined`). | `AvatarSessionCreateResponse` |
| `400` | `invalid_request` (only one of `livekitUrl` / `livekitToken` sent), `invalid_livekit_url` (not a ws(s):// server URL), `invalid_livekit_token` (not a LiveKit access token, no room grant, or expired), `missing_publish_on_behalf` (the token names no agent to render for), `agent_identity_mismatch` (`agentIdentity` differs from the token's `lk.publish_on_behalf`). | `ApiErrorResponse` |
| `401` | Missing or invalid API key (a user token is refused here) | none |
| `402` | Out of credits, this key's spend cap was reached, or too many sessions at once for this key | `PaymentRequiredResponse` |
| `403` | `avatar_not_allowed` — the avatar is not on this key's allow-list | `ApiErrorResponse` |
| `409` | `avatar_not_renderable` — the avatar has nothing to render: no reference image (v1) and no v2 bundle \| `avatar_busy` — the rendering capacity refused the start while the avatar is in another live session of this account (`liveSessionId` when it is an API session: end it with `POST /v1/avatar_sessions/{id}/end`, or retry once it has ended); nothing is billed | `ApiErrorResponse` |
| `422` | Request body failed schema validation. | `FrameworkErrorResponse` |
| `502` | `avatar_start_failed` — the rendering worker refused the render before the avatar joined (for example the portrait could not be cropped to a face). `message` carries the worker's text. Nothing is billed. | `ApiErrorResponse` |
| `503` | `no_capacity` — every rendering worker is busy; retry after the `Retry-After` seconds. `avatar_warming` — a v2 avatar is being prepared on a rendering node; retry after the `Retry-After` seconds (nothing is billed). `avatar_start_interrupted` — the rendering worker accepted the start, but it ended before the avatar joined your room; nothing is billed, retry after the `Retry-After` seconds. `avatar_version_unavailable` — the avatar is a v2 avatar and this deployment renders v1 only. `avatar_sessions_disabled` — this deployment has no rendering backend configured. `hosted_room_unavailable` — `livekitUrl` and `livekitToken` were omitted and this deployment cannot host a room. `embed_disabled` — API-key sessions are switched off. | `ApiErrorResponse` |

## `202` response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sessionId` | string | yes |  |
| `status` | string (`initializing`, `avatar_joined`) | yes | How far the start had progressed when this response was sent. |
| `avatarParticipantIdentity` | string | yes | The identity the avatar joins the room with (from your token; `atmee-avatar-agent` in a hosted room). |
| `agentIdentity` | string | yes | The agent the avatar renders for (the token's `lk.publish_on_behalf`, or the hosted room's agent). |
| `roomName` | string | yes | The room, as granted by your token or created by Atmee. |
| `maxDurationSeconds` | integer | yes | The hard ceiling applied to this session. |
| `billingMode` | string (`reserve`, `metered`) | yes |  |
| `livekitUrl` | string | no | Atmee-hosted room only: the LiveKit server (`wss://...`) your agent joins. |
| `agentToken` | string | no | Atmee-hosted room only: the access token your agent joins the room with (identity `agentIdentity`, publish, subscribe and data). Valid for the session's ceiling plus the join timeout; treat it as a secret. |
