Realtime API
sessions/start is the single call you make to start a realtime conversation. It creates the room, brings in the AI agent (and an avatar, if you want one), and returns everything your app needs to join: a room name, a JWT access token, and the server URL.
Endpoint
POST https://api.lookalike.com/v1/sessions/start
Request
Send a JSON body. Every field is optional — an empty body {} starts a default session. To start an avatar session you only need identity_id: supplying it turns the avatar on automatically, so you don't also have to set avatar_enabled.
| Field | Type | Description |
|---|---|---|
identity_id | string | Onboarded persona/identity id to render as the talking-head avatar. Supplying it implies avatar_enabled: true. |
avatar_enabled | boolean | Set to true to add a talking-head avatar video. Implied when identity_id is set. |
language | string | Per-session conversation language: auto, en, es, pl, or uk. Omit it to use the identity's stored language. |
visual_awareness | boolean | Let the agent see the visitor: while they speak, frames from their camera accompany their words as image input, so the Lookalike can react to what it sees. Omit it for the account default (currently on for every identity-backed session), or send false to opt the session out. The response carries the effective value. |
form_factor | string | The visitor's device class: phone or desktop. The avatar video is rendered in a shape suited to it: portrait for phone, and landscape for desktop where landscape video is enabled (until then, desktop sessions get the default portrait video). Omit it to get the default shape. Any other value returns 422 before the room is created. |
Everything else about the persona is resolved from the identity itself: which renderer serves it, which image it wears, and which cloned voice it speaks with. There is no field to override any of them, and none is needed - sending a different identity_id changes all three together.
Conversation language
The effective conversation language is selected in this order:
languageinPOST /v1/sessions/start- the language stored on the identity
en, when the call supplies no identity to read a language from
Passing "language": "auto" is an explicit override: the greeting is English and later replies follow the visitor's detected language. A session override never changes the identity - read it back with GET /v1/identities/{identity_id} and it is still whatever you stored.
{ "identity_id": "test-identity-03d39af9", "language": "es" }
Swap es for pl, uk, en, or auto. A pinned language is a hard constraint: the agent answers only in it, even when the visitor switches. Anything outside the five values returns 422 before the room is created.
Every mode except en needs an identity with an ElevenLabs voice, and that includes auto - letting the visitor pick the language is as multilingual a demand as pinning one. Without such a voice the API returns 422 rather than falling back to an English-only voice that would read the words with the wrong phonemes.
Example
The minimal call — just the persona id:
curl -X POST https://api.lookalike.com/v1/sessions/start \
-H "Authorization: Bearer $LOOKALIKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity_id": "test-identity-03d39af9"}'
Response
200 OK:
{
"room_name": "room-user-001-6014c7ef",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"livekit_url": "wss://your-livekit-server",
"dispatch_id": "AD_xxxxxxxxxxxx",
"user": { "id": "acct_ab12cd34" },
"created_at": "2026-06-17T09:10:27.994123+00:00",
"visual_awareness": true
}
The three fields you need to join are room_name, token, and livekit_url. The token already encodes the room and your identity — you don't sign or mint anything yourself.
visual_awareness tells your client whether this session's agent will use the visitor's camera. When it is true, offer a camera control and publish an ordinary camera track when the visitor turns it on (the agent samples it only while they speak). When it is false, don't offer one — frames would be ignored.
Errors
| Status | Meaning |
|---|---|
401 Unauthorized | Missing or invalid API key. |
422 Unprocessable Entity | The request body is malformed. |
500 Internal Server Error | Something went wrong on our side — retry shortly. |
Join the room
The token is a standard LiveKit access token. Connect with any LiveKit client SDK using livekit_url + token; the room is already set up and the agent is on its way in.
- JavaScript
- React
- Python
import { Room, RoomEvent, Track } from 'livekit-client';
// livekitUrl and token come from the sessions/start response
const room = new Room();
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Video) {
// attach the avatar video track to a view element
document.getElementById('avatar').appendChild(track.attach());
} else if (track.kind === Track.Kind.Audio) {
// play the agent audio track
document.body.appendChild(track.attach());
}
});
await room.connect(livekitUrl, token);
// enable the local mic so the agent can hear the user
await room.localParticipant.setMicrophoneEnabled(true);
import { LiveKitRoom, RoomAudioRenderer, VideoTrack, useTracks } from '@livekit/components-react';
import { Track } from 'livekit-client';
import '@livekit/components-styles';
// livekitUrl and token come from the sessions/start response
function AvatarView() {
// remote camera tracks (the avatar video), already subscribed by default
const tracks = useTracks([Track.Source.Camera], { onlySubscribed: true });
return <>{tracks.map((t) => <VideoTrack key={t.publication.trackSid} trackRef={t} />)}</>;
}
export default function Room({ livekitUrl, token }: { livekitUrl: string; token: string }) {
return (
// audio={true} enables the local mic so the agent can hear the user
<LiveKitRoom serverUrl={livekitUrl} token={token} connect={true} audio={true}>
<AvatarView />
<RoomAudioRenderer /> {/* plays the agent audio track */}
</LiveKitRoom>
);
}
import asyncio
from livekit import rtc
# livekit_url and token come from the sessions/start response (token is pre-minted server-side).
async def join_room(livekit_url: str, token: str) -> rtc.Room:
room = rtc.Room()
@room.on("track_subscribed")
def on_track_subscribed(track, publication, participant):
if track.kind == rtc.TrackKind.KIND_VIDEO:
# Avatar video: iterate frames and forward them to your view/renderer.
asyncio.ensure_future(_consume(rtc.VideoStream(track)))
elif track.kind == rtc.TrackKind.KIND_AUDIO:
# Agent audio: iterate frames and push them to your audio sink.
asyncio.ensure_future(_consume(rtc.AudioStream(track)))
# Autosubscribe is on by default, so remote tracks arrive via track_subscribed.
await room.connect(livekit_url, token)
return room
async def _consume(stream):
try:
async for frame in stream:
pass # render video / play audio here
finally:
await stream.aclose()
End the session
A session ends when the client disconnects from the LiveKit room. You can also end it explicitly from your backend, without waiting for the client to drop:
POST https://api.lookalike.com/v1/sessions/{session_id}/end
Pass the room_name from the sessions/start response as the session_id. No request body is required.
curl -X POST https://api.lookalike.com/v1/sessions/room-user-001-6014c7ef/end \
-H "Authorization: Bearer $LOOKALIKE_API_KEY"
200 OK:
{
"session_id": "room-user-001-6014c7ef",
"status": "ended",
"identity_id": null,
"pipeline_id": null,
"mode": "meeting_agent",
"metadata": null
}
The call closes the room and disconnects every participant, including the agent. It is idempotent: ending a session that is already gone still returns 200 OK, so it is safe to call after the client has disconnected on its own.
Errors
| Status | Meaning |
|---|---|
401 Unauthorized | Missing or invalid API key. |
404 Not Found | The session does not exist, or it belongs to another account. |
502 Bad Gateway | We could not confirm the room was closed - retry shortly. |
End-to-end example
import os
import httpx
from livekit import rtc
BASE_URL = "https://api.lookalike.com"
API_KEY = os.environ["LOOKALIKE_API_KEY"]
async def start_session() -> rtc.Room:
"""Start a session with just the persona id and join it with the returned token."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{BASE_URL}/v1/sessions/start",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"identity_id": "test-identity-03d39af9"},
)
resp.raise_for_status()
data = resp.json()
room = rtc.Room()
await room.connect(data["livekit_url"], data["token"])
return room
Notes
- Avatar audio. When an avatar is enabled it publishes both audio and video. Don't also play the agent's raw voice track, or you'll hear it twice.
- One token per session. The token is issued per call — start a fresh room for each new session.
- Ending a session. Disconnect from the LiveKit room, or call
POST /v1/sessions/{session_id}/endto end it from your backend. - Avatar usage is billed to your account. If you run out of credits, the
session ends with an
insufficient_creditsnotice.