Migrate from Tavus
Tavus is a great product and we respect their team. If you're evaluating a move to Lookalike, this guide maps the calls you already make onto ours, side by side, so you can see exactly what changes. The Tavus snippets are shown for orientation against its v2 API.
Two things are worth knowing up front:
- One object instead of two. In Tavus you manage a replica for the face and a separate persona for the personality, then pair them per conversation. Here they are a single identity: face, voice, and personality in one object you create once and reuse by
identity_id. - LiveKit, not Pipecat. A Lookalike session runs over LiveKit. You start a session, get a room name, an access token, and a server URL back, and join with a LiveKit client. If your Tavus integration is built on Pipecat (Daily's open-source framework), swapping its client for the LiveKit client is the main change on the browser side.
How the concepts map
| Tavus | Lookalike | Notes |
|---|---|---|
| Replica | part of an identity | The visual likeness |
| Persona | part of an identity | Its system_prompt becomes the identity prompt |
| Voice on the persona | part of an identity | A reference audio clip |
| Replica + persona | one identity_id | Created and trained in a single call |
| Conversation | session | POST /v1/sessions/start |
conversation_url (Pipecat) | room_name + token + livekit_url | Join with a LiveKit client |
x-api-key header | Authorization: Bearer or X-API-Key | Keys start with lk_ |
| Conversational minutes | credits | One balance covers onboarding and realtime |
Authenticate
Tavus sends x-api-key; Lookalike sends a bearer token (or X-API-Key), and keys start with lk_. Get one in the console.
Tavus
curl https://tavusapi.com/v2/replicas -H "x-api-key: $TAVUS_API_KEY"
Lookalike
curl https://api.lookalike.com/v1/identities -H "Authorization: Bearer $LOOKALIKE_API_KEY"
Create the avatar
In Tavus this is two objects and an asynchronous wait: train a replica from a consent video, then create a persona bound to it. In Lookalike it is one multipart call that carries the face, the voice, and the personality, and trains inline — your Tavus persona prompt moves over unchanged as prompt.
Tavus
# 1. train a replica from a consent video (asynchronous — poll until ready)
curl -sS -X POST https://tavusapi.com/v2/replicas \
-H "x-api-key: $TAVUS_API_KEY" -H "Content-Type: application/json" \
-d '{ "replica_name": "Ada", "train_video_url": "https://example.com/ada-consent.mp4" }'
# -> { "replica_id": "r1a2b3c4", "status": "training" }
# 2. create a persona bound to that replica
curl -sS -X POST https://tavusapi.com/v2/personas \
-H "x-api-key: $TAVUS_API_KEY" -H "Content-Type: application/json" \
-d '{ "persona_name": "Ada",
"system_prompt": "You are Ada, a warm and concise product guide.",
"default_replica_id": "r1a2b3c4" }'
# -> { "persona_id": "p5f6g7h8" }
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.
With a photo our create returns ready inline, so you usually skip the poll step Tavus requires after training a replica. To re-check later, GET /v1/identities/{identity_id}/status.
Start a session
Tavus creates a conversation from a replica_id plus a persona_id and returns a conversation_url. Lookalike starts a session from just the identity_id and returns the LiveKit room name, token, and URL.
Tavus
curl -sS -X POST https://tavusapi.com/v2/conversations \
-H "x-api-key: $TAVUS_API_KEY" -H "Content-Type: application/json" \
-d '{ "replica_id": "r1a2b3c4", "persona_id": "p5f6g7h8" }'
# -> { "conversation_id": "c9i8h7g6", "conversation_url": "https://.../c9i8h7g6" }
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, so your browser never sees the API key. See Realtime API for the full response.
Pick the conversation language
Tavus sets the language under the conversation's properties. Lookalike takes it as a top-level language on the session, and falls back to the language stored on the identity when you omit it.
const s = await fetch(`${LOOKALIKE}/v1/sessions/start`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ identity_id, language: 'es' }),
});
const { room_name, token, livekit_url } = await s.json();
Supported values are auto, en, es, pl, and uk. The other Tavus conversation properties do not map: max_call_duration remains unsupported, so end a session yourself when you need a hard limit.
Join the call
Tavus hands you a conversation_url that you open in your Pipecat client. Lookalike hands you livekit_url + token that you join with a LiveKit client.
Lookalike
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
The agent listens and responds on its own, handling turns and interruptions natively — there is no push-to-talk. Transcripts arrive through LiveKit's own client SDK.
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.
End the session
Tavus ends a conversation with an explicit call. Lookalike gives you both options: disconnect from the room, or call the End Session endpoint.
Tavus
curl -sS -X POST https://tavusapi.com/v2/conversations/c9i8h7g6/end \
-H "x-api-key: $TAVUS_API_KEY"
Lookalike
Disconnect the client:
await room.disconnect();
Or end it from your backend, passing the room_name from sessions/start as the session_id:
curl -sS -X POST https://api.lookalike.com/v1/sessions/ROOM_NAME/end \
-H "Authorization: Bearer $LOOKALIKE_API_KEY"
Billing
Tavus meters conversational minutes. Lookalike meters credits from one balance: onboarding an identity is a one-time charge, and a realtime session is charged per minute while the avatar is streaming. Buy credits and view usage in the Console; full rates are on the Billing & credits page. A request that would spend credits you don't have returns 402, and a live session that runs out ends on its own with an insufficient_credits notice.
What's different
- One identity, not a replica plus persona. You manage a single object. Updating its prompt, look, or voice retrains it, so send those inputs together, the same way you created it.
- Create is synchronous with a photo. No training webhook to wait on for the common case.
- Local file upload. You upload the reference image directly, rather than hosting it at a public URL first.
Next steps
- Core concepts — how identities and sessions fit together.
- Create an Identity — the full create/train reference.
- Realtime API — the full session and join reference.
- Billing & credits — rates and how credits are charged.