An AI you can talk to, face to face
The embody API puts a full-body, real-time video avatar on any screen. She listens, answers in her own voice, moves through her room and sees the viewer through their camera. You describe who she is — a persona — and open a session for each viewer. Everything else runs in the player.
Creates a session with your secret API key and gets back a one-time URL.
Opens the URL in an iframe. Microphone and camera stay between the viewer and the model.
Streams the avatar, speaks as your persona, and meters the minutes the conversation is open.
https://app.embody.cam/api/v1Quickstart
From nothing to a live avatar in three requests.
- Get a key. Sign in with Google — the first 30 minutes are free; after that, attach a card in Usage & billing. Keep the key in an environment variable on your server —
EMBODY_API_KEYin every sample here. - Create a persona. Give her a name, a prompt and a voice. You do this once; the same persona serves every viewer.
- Open a session per viewer. Your server asks for one and passes the returned
urlto the page. - Embed the URL. The viewer presses “Start talking” and the conversation begins. Billing starts at that moment, not when the frame loads.
curl https://app.embody.cam/api/v1/personas \ -H "Authorization: Bearer $EMBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Aria", "prompt": "Your name is Aria. You greet visitors to our studio and help them book a call.", "voice": "coral" }'
curl https://app.embody.cam/api/v1/sessions \ -H "Authorization: Bearer $EMBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "persona_id": "per_6fQm0cXk2LrT8bNw1sYa" }'
<iframe src="SESSION_URL" allow="microphone; camera; autoplay; fullscreen" style="width: 100%; aspect-ratio: 16 / 9; border: 0" ></iframe>
Authentication
Every request carries your secret key as a bearer token. Keys start with emb_live_ and are shown once, when created — embody keeps only a hash.
Make separate keys for production, staging and development so one can be revoked without touching the others. A revoked key stops working at once; sessions it already opened run to their end.
curl https://app.embody.cam/api/v1/personas \ -H "Authorization: Bearer $EMBODY_API_KEY"
Writing a persona
The prompt is who she is: a name, a role, what she knows, what she is for, and how she talks. Write it in the second person, the way you would brief a new host on their first day.
Her body, her room and her movements come from the motion set and are the same for every persona. Describe the person, not the place — a prompt that re-describes the room only contradicts what is on screen.
- Keep answers short. Spoken, a paragraph feels like a lecture.
- Say what she should do when she does not know — otherwise she guesses.
- Describe how she sounds. The voice preset picks a timbre; pace, warmth and humour are in the prompt.
- Edits apply to sessions opened afterwards. Sessions already live keep their version.
Your name is Aria. You are the front-desk host for Studio North, a small design studio in Riga. You greet visitors, tell them what the studio does — brand identity and websites for restaurants — and help them book a call with Marta, the founder. Be warm and brief, the way people really talk: one or two sentences at a time. Ask one question, then listen. If you do not know something, say so and offer to have Marta follow up. How you sound: relaxed and friendly, with a smile in your voice. Never rush.
Session lifecycle
Returned by Create a session. The URL is valid for an hour; nothing is billed.
The viewer pressed “Start talking”. The player sends a heartbeat every 20 s and each one is billed.
The viewer hung up or closed the tab, you stopped it, 90 s passed without a heartbeat, or an hour passed unopened.
A session is one conversation with one viewer. Once ended it cannot be reopened — create a new one. Open sessions per viewer and on demand, when the page that shows the avatar is requested; an unopened session costs nothing but expires after an hour.
Embedding the player
The session URL is a complete player. Put it in an iframe and give it the permissions it asks for:
microphone— required. The viewer talks to her; without it they can still type.camera— optional. With it she sees the viewer and reacts to what they do. Frames are described on the fly and never stored.autoplay— her voice starts without a second click.
The picture is 16:9 and letterboxed inside any other shape. Browsers only grant the microphone on HTTPS pages (and on localhost).
<iframe src="SESSION_URL" allow="microphone; camera; autoplay; fullscreen" style="width: 100%; aspect-ratio: 16 / 9; border: 0" ></iframe>
export function Avatar({ url }: { url: string }) { return ( <iframe src={url} allow="microphone; camera; autoplay; fullscreen" style={{ width: "100%", aspectRatio: "16 / 9", border: 0 }} /> ) }
Billing
embody is pay as you go. Minutes are counted while a session is live — from “Start talking” to hang-up — and a started minute counts as a whole one. Usage goes to Stripe as it happens and is invoiced monthly on the card in Usage & billing.
Every account starts with 30 free minutes, spent before anything is metered, and no card is needed while they last.
Once they are spent and there is no card, Create a session answers 402 payment_required. The same check runs when the viewer opens the URL and on every heartbeat, so a live session is hung up when the last free minute goes, and removing the card stops sessions that have not started yet.
The persona object
Attributes
- idstringrequiredStarts with
per_. - objectstringrequiredAlways
persona. - namestringrequiredWhat she calls herself in the console. The prompt decides what she says.
- promptstringrequiredThe system prompt — who she is and how she talks.
- voiceenumrequiredOne of the voices.
- speednumberrequiredSpeaking pace, 0.25–1.5. 1 is normal.
- created_at, updated_attimestamprequiredISO 8601, UTC.
{
"id": "per_6fQm0cXk2LrT8bNw1sYa",
"object": "persona",
"name": "Aria",
"prompt": "Your name is Aria. You are the front-desk host…",
"voice": "coral",
"speed": 1.05,
"created_at": "2026-10-22T16:04:11.000Z",
"updated_at": "2026-10-22T16:04:11.000Z"
}Create a persona
Body parameters
- namestringrequiredUp to 60 characters.
- promptstringrequiredSee Writing a persona.
- voiceenumoptionalDefaults to
marin. - speednumberoptional0.25–1.5, defaults to
1. Values outside are clamped.
curl https://app.embody.cam/api/v1/personas \ -H "Authorization: Bearer $EMBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Aria", "prompt": "Your name is Aria. You greet visitors to our studio and help them book a call.", "voice": "coral" }'
{
"id": "per_6fQm0cXk2LrT8bNw1sYa",
"object": "persona",
"name": "Aria",
"prompt": "Your name is Aria. You are the front-desk host…",
"voice": "coral",
"speed": 1.05,
"created_at": "2026-10-22T16:04:11.000Z",
"updated_at": "2026-10-22T16:04:11.000Z"
}List personas
All personas on the account, most recently edited first.
curl https://app.embody.cam/api/v1/personas \ -H "Authorization: Bearer $EMBODY_API_KEY"
{
"object": "list",
"data": [ { "id": "per_…", "object": "persona", … } ]
}Retrieve a persona
Returns the persona object, or 404 if there is none with this id on your account.
curl https://app.embody.cam/api/v1/personas/per_6fQm0cXk2LrT8bNw1sYa \ -H "Authorization: Bearer $EMBODY_API_KEY"
Update a persona
Only the fields you send change. Sessions already live keep the version they started with.
Body parameters
- namestringoptionalNew name.
- promptstringoptionalNew prompt.
- voiceenumoptionalNew voice.
- speednumberoptionalNew pace.
curl -X PATCH https://app.embody.cam/api/v1/personas/per_6fQm0cXk2LrT8bNw1sYa \ -H "Authorization: Bearer $EMBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice": "shimmer", "speed": 0.95 }'
Delete a persona
curl -X DELETE https://app.embody.cam/api/v1/personas/per_6fQm0cXk2LrT8bNw1sYa \ -H "Authorization: Bearer $EMBODY_API_KEY"
{
"id": "per_6fQm0cXk2LrT8bNw1sYa",
"object": "persona",
"deleted": true
}The session object
Attributes
- idstringrequiredStarts with
ses_. - objectstringrequiredAlways
session. - persona_idstringrequiredThe persona she speaks as.
- statusenumrequired
- secondsintegerrequiredTime the conversation has been open.
- billed_minutesintegerrequiredMinutes sent to billing so far.
- url, tokenstringoptionalOnly in the Create response. The token is part of the URL.
- created_at, started_at, ended_attimestamprequiredNull until it happens.
{
"id": "ses_Kp3v9QeW1zTn6RyU0mBd",
"object": "session",
"persona_id": "per_6fQm0cXk2LrT8bNw1sYa",
"status": "live",
"seconds": 184,
"billed_minutes": 3,
"created_at": "2026-10-22T16:10:02.000Z",
"started_at": "2026-10-22T16:10:09.000Z",
"ended_at": null
}Create a session
Body parameters
- persona_idstringrequiredWhich persona she speaks as.
Returns the session with its url — the only time the URL is returned. Answers 402 when the free minutes are spent and no card is attached.
curl https://app.embody.cam/api/v1/sessions \ -H "Authorization: Bearer $EMBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "persona_id": "per_6fQm0cXk2LrT8bNw1sYa" }'
{
"id": "ses_Kp3v9QeW1zTn6RyU0mBd",
"object": "session",
"persona_id": "per_6fQm0cXk2LrT8bNw1sYa",
"status": "created",
"url": "https://embed.embody.cam/?session=st_…",
"token": "st_…",
"expires_at": "2026-10-22T17:10:02.000Z"
}List sessions
The 50 most recent sessions, newest first, with their status and minutes.
curl https://app.embody.cam/api/v1/sessions \ -H "Authorization: Bearer $EMBODY_API_KEY"
Retrieve a session
The current state of one session. Poll it if your app needs to know when a conversation has ended — it reads ended within 90 seconds of the viewer leaving.
curl https://app.embody.cam/api/v1/sessions/ses_Kp3v9QeW1zTn6RyU0mBd \ -H "Authorization: Bearer $EMBODY_API_KEY"
Stop a session
Ends the session now and bills the started minute. The player hangs up on its next heartbeat, within 20 seconds. Stopping a session that has already ended does nothing.
curl -X POST https://app.embody.cam/api/v1/sessions/ses_Kp3v9QeW1zTn6RyU0mBd/stop \ -H "Authorization: Bearer $EMBODY_API_KEY"
Voices
| voice | character |
|---|---|
| marin | warm, natural — the player's default |
| coral | bright, friendly |
| shimmer | soft, light |
| sage | calm, even |
| ballad | expressive, melodic |
| alloy | neutral, balanced |
| verse | versatile, animated |
| cedar | deep, grounded |
| ash | clear, direct |
| echo | resonant, steady |
The voice sets the timbre only. Accent, warmth, pace and manner are asked for in the prompt, and she answers in whatever language the viewer speaks.
Errors
Errors use HTTP status codes and one body shape everywhere. Branch on type; show message to people. param names the field at fault, when there is one.
| 400 | invalid_request | A field is missing or has the wrong value. |
| 401 | unauthorized | No key, a malformed one, or a revoked one. |
| 402 | payment_required | Free minutes spent and no card on the account. |
| 404 | not_found | No such object on your account. |
| 410 | session_ended | The session is over; create a new one. |
| 500 | server_error | Something failed on our side. Safe to retry. |
{
"error": {
"type": "invalid_request",
"message": "persona_id is required",
"param": "persona_id"
}
}const res = await fetch(url, init) if (!res.ok) { const { error } = await res.json() if (error.type === "payment_required") { // ask the account owner to attach a card } throw new Error(`${error.type}: ${error.message}`) }