Migrate from HeyGen
HeyGen is a strong platform and we respect their team. Because HeyGen's interactive avatars already run over LiveKit — the same transport we use — most of your client-side code carries over, and the migration is mainly about a simpler object model and a shorter session handshake. The HeyGen snippets reflect its streaming.* API and are shown for orientation.
Two things are worth knowing up front:
- One object instead of two. HeyGen binds an
avatar_id(the look) and a separatevoice_id(the sound) per session. Here they are a single identity: face, voice, and personality in one object you create once and reuse byidentity_id. - One call instead of a handshake. HeyGen's streaming path is
streaming.create_token→streaming.new→streaming.start, and you then drive speech with repeatedstreaming.taskcalls. Lookalike is onesessions/start, and the agent converses on its own — no per-utterance calls.
How the concepts map
| HeyGen | Lookalike | Notes |
|---|---|---|
Avatar (avatar_id) | part of an identity | The look |
Voice (voice_id) | part of an identity | The sound |
avatar_id + voice_id (two refs) | one identity_id | Bundled and reused |
streaming.create_token + streaming.new + streaming.start | POST /v1/sessions/start | One call |
streaming.task (repeat / talk) | native conversation | The agent speaks on its own |
streaming.stop | disconnect or POST /v1/sessions/{session_id}/end | Leave the LiveKit room, or end it explicitly |
X-Api-Key header | Authorization: Bearer or X-API-Key | Keys start with lk_ |
| Usage credits / concurrent sessions | credits | One balance for onboarding and realtime |
Authenticate
HeyGen sends X-Api-Key against https://api.heygen.com. Lookalike sends a bearer token (or X-API-Key) against https://api.lookalike.com, and keys start with lk_. Get one in the console.
HeyGen
curl https://api.heygen.com/v1/streaming/avatar.list -H "X-Api-Key: $HEYGEN_API_KEY"
Lookalike
curl https://api.lookalike.com/v1/identities -H "Authorization: Bearer $LOOKALIKE_API_KEY"
Create the avatar
In HeyGen you reference an avatar_id (a stock interactive avatar, or a Photo/Instant avatar you created) plus a separate voice_id, and wire them together at session time. In Lookalike you create one identity — face, optional voice clip, and personality prompt — in a single call that trains inline and returns ready.
Lookalike
- cURL
- Python
- Node.js
curl -sS -X POST https://api.lookalike.com/v1/identities \
-H "Authorization: Bearer $LOOKALIKE_API_KEY" \
-F "name=Ada" \
-F "prompt=You are Ada, a warm and concise product guide." \
-F "image=@ada.jpg;type=image/jpeg" \
-F "audio=@ada.wav;type=audio/wav"
# -> { "identity_id": "id_...", "status": "ready" }
import httpx
with open("ada.jpg", "rb") as img, open("ada.wav", "rb") as aud:
resp = httpx.post(
"https://api.lookalike.com/v1/identities",
headers={"Authorization": f"Bearer {API_KEY}"},
data={
"name": "Ada",
"prompt": "You are Ada, a warm and concise product guide.",
},
files=[
("image", ("ada.jpg", img, "image/jpeg")),
("audio", ("ada.wav", aud, "audio/wav")),
],
)
resp.raise_for_status()
identity = resp.json() # { identity_id, status: "ready", ... }
import { openAsBlob } from 'node:fs';
const form = new FormData();
form.set('name', 'Ada');
form.set('prompt', 'You are Ada, a warm and concise product guide.');
form.set('image', await openAsBlob('ada.jpg', { type: 'image/jpeg' }), 'ada.jpg');
form.set('audio', await openAsBlob('ada.wav', { type: 'audio/wav' }), 'ada.wav');
const resp = await fetch('https://api.lookalike.com/v1/identities', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.LOOKALIKE_API_KEY}` },
body: form,
});
const identity = await resp.json(); // { identity_id, status: "ready", ... }
See Create an Identity for the full field list.
Start a session
HeyGen mints a short-lived token, opens a session against an avatar_id + voice, then starts it. Lookalike takes just the identity_id and returns the LiveKit room, token, and URL directly.
HeyGen
# 1. mint a short-lived client token
curl -sS -X POST https://api.heygen.com/v1/streaming.create_token \
-H "X-Api-Key: $HEYGEN_API_KEY"
# -> { "data": { "token": "..." } }
# 2. open the streaming session (LiveKit-backed)
curl -sS -X POST https://api.heygen.com/v1/streaming.new \
-H "X-Api-Key: $HEYGEN_API_KEY" -H "Content-Type: application/json" \
-d '{ "version": "v2", "quality": "high",
"avatar_id": "Ada_public",
"voice": { "voice_id": "en-US-...", "rate": 1.0 } }'
# -> { "data": { "session_id": "...", "url": "wss://heygen-xxxx.livekit.cloud",
# "access_token": "..." } }
# 3. start it
curl -sS -X POST https://api.heygen.com/v1/streaming.start \
-H "X-Api-Key: $HEYGEN_API_KEY" -H "Content-Type: application/json" \
-d '{ "session_id": "..." }'
Lookalike
curl -sS -X POST https://api.lookalike.com/v1/sessions/start \
-H "Authorization: Bearer $LOOKALIKE_API_KEY" -H "Content-Type: application/json" \
-d '{ "identity_id": "id_3f9a2b7c4d1e8a05f6b3" }'
# -> { "room_name": "...", "token": "...", "livekit_url": "wss://..." }
The token is minted server-side and already encodes the room. See Realtime API for the full response.
Join the call
Both HeyGen and Lookalike hand you a LiveKit URL and token, so your join code is the same — point your LiveKit client at ours (livekit_url + token) instead of HeyGen's url + access_token.
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) => {
if (track.kind === Track.Kind.Video) {
document.getElementById('avatar').appendChild(track.attach());
} else if (track.kind === Track.Kind.Audio) {
document.body.appendChild(track.attach());
}
});
await room.connect(livekitUrl, token);
await room.localParticipant.setMicrophoneEnabled(true); // so the avatar can hear you
When an avatar is enabled it publishes both audio and video. Attach both, but don't also play the agent's raw voice track, or you'll hear it twice.