embody

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.

1
Your server

Creates a session with your secret API key and gets back a one-time URL.

2
The viewer's browser

Opens the URL in an iframe. Microphone and camera stay between the viewer and the model.

3
embody

Streams the avatar, speaks as your persona, and meters the minutes the conversation is open.

Base URLhttps://app.embody.cam/api/v1

Quickstart

From nothing to a live avatar in three requests.

  1. 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_KEY in every sample here.
  2. Create a persona. Give her a name, a prompt and a voice. You do this once; the same persona serves every viewer.
  3. Open a session per viewer. Your server asks for one and passes the returned url to the page.
  4. Embed the URL. The viewer presses “Start talking” and the conversation begins. Billing starts at that moment, not when the frame loads.
Try it without codeEvery persona in the console has a Go live button that opens exactly the session the API would.
1 · Create a persona
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"
  }'
2 · Open a session
curl https://app.embody.cam/api/v1/sessions \
  -H "Authorization: Bearer $EMBODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "persona_id": "per_6fQm0cXk2LrT8bNw1sYa" }'
3 · Embed it
<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.

Keys belong on your serverA key can create sessions billed to your card. Never put it in a page, an app bundle or a repository. The browser needs only the session URL, which is good for one conversation.

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.
A good prompt
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

created

Returned by Create a session. The URL is valid for an hour; nothing is billed.

live

The viewer pressed “Start talking”. The player sends a heartbeat every 20 s and each one is billed.

ended

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).

The URL is a credentialAnyone with the URL can hold that one conversation. Create it for the viewer who is about to see it; do not cache it or share it between viewers.
HTML
<iframe
  src="SESSION_URL"
  allow="microphone; camera; autoplay; fullscreen"
  style="width: 100%; aspect-ratio: 16 / 9; border: 0"
></iframe>
React
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

  • idstringrequired
    Starts with per_.
  • objectstringrequired
    Always persona.
  • namestringrequired
    What she calls herself in the console. The prompt decides what she says.
  • promptstringrequired
    The system prompt — who she is and how she talks.
  • voiceenumrequired
    One of the voices.
  • speednumberrequired
    Speaking pace, 0.25–1.5. 1 is normal.
  • created_at, updated_attimestamprequired
    ISO 8601, UTC.
Persona
{
  "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

POST/api/v1/personas

Body parameters

  • namestringrequired
    Up to 60 characters.
  • promptstringrequired
  • voiceenumoptional
    Defaults to marin.
  • speednumberoptional
    0.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"
  }'
Response · 201
{
  "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

GET/api/v1/personas

All personas on the account, most recently edited first.

curl https://app.embody.cam/api/v1/personas \
  -H "Authorization: Bearer $EMBODY_API_KEY"
Response
{
  "object": "list",
  "data": [ { "id": "per_…", "object": "persona", … } ]
}

Retrieve a persona

GET/api/v1/personas/:id

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

PATCH/api/v1/personas/:id

Only the fields you send change. Sessions already live keep the version they started with.

Body parameters

  • namestringoptional
    New name.
  • promptstringoptional
    New prompt.
  • voiceenumoptional
    New voice.
  • speednumberoptional
    New 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

DELETE/api/v1/personas/:id
Deletes its sessions tooThe persona's session history goes with it. Minutes already billed stay on your invoice.
curl -X DELETE https://app.embody.cam/api/v1/personas/per_6fQm0cXk2LrT8bNw1sYa \
  -H "Authorization: Bearer $EMBODY_API_KEY"
Response
{
  "id": "per_6fQm0cXk2LrT8bNw1sYa",
  "object": "persona",
  "deleted": true
}

The session object

Attributes

  • idstringrequired
    Starts with ses_.
  • objectstringrequired
    Always session.
  • persona_idstringrequired
    The persona she speaks as.
  • statusenumrequired
    created, live or ended — see lifecycle.
  • secondsintegerrequired
    Time the conversation has been open.
  • billed_minutesintegerrequired
    Minutes sent to billing so far.
  • url, tokenstringoptional
    Only in the Create response. The token is part of the URL.
  • created_at, started_at, ended_attimestamprequired
    Null until it happens.
Session
{
  "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

POST/api/v1/sessions

Body parameters

  • persona_idstringrequired
    Which 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" }'
Response · 201
{
  "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

GET/api/v1/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

GET/api/v1/sessions/:id

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

POST/api/v1/sessions/:id/stop

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

voicecharacter
marinwarm, natural — the player's default
coralbright, friendly
shimmersoft, light
sagecalm, even
balladexpressive, melodic
alloyneutral, balanced
verseversatile, animated
cedardeep, grounded
ashclear, direct
echoresonant, 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.

400invalid_requestA field is missing or has the wrong value.
401unauthorizedNo key, a malformed one, or a revoked one.
402payment_requiredFree minutes spent and no card on the account.
404not_foundNo such object on your account.
410session_endedThe session is over; create a new one.
500server_errorSomething failed on our side. Safe to retry.
Error body
{
  "error": {
    "type": "invalid_request",
    "message": "persona_id is required",
    "param": "persona_id"
  }
}
Handling
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}`)
}