Embed
Embed
<avatar-embed> is the lowest-effort way to put a live avatar on a page: one script
tag, a publishable key, and a stock persona. The element renders a responsive 16:9
panel with its own Start button, handles the microphone, the queue, and the seat
lifecycle, and needs no backend of your own.
<script src="https://www.casola.ai/embed.js" data-key="pk_abc12345.def6789…" data-avatar="david" data-lang="en"></script>That is the whole integration. If you need a custom avatar, your own UI, or a system prompt, you want the Quickstart instead — those all require a secret key, which must never reach a browser.
1. Create a publishable key
data-key takes a publishable key (pk_…), never a secret one. Publishable keys
are locked to the session:connect scope and to a list of origins you register, which
is what makes them safe in page source. Create one in the
dashboard, or via the API — see
Creating a publishable key.
Register every origin the embed will run on, scheme and all: https://example.com and
https://www.example.com are different origins. An unregistered origin fails the
browser’s preflight, and the panel reports it as a network error.
2. Pick an avatar
data-avatar names a stock persona from the public catalog:
curl https://api.casola.ai/api/galleryAny id in that response works. See
Gallery for what each one carries.
Custom avatars are addressed with avatar_id / avatar_ref, which are secret-key
only, so they are out of reach from a browser-side embed by design. Mint those
server-side and connect with the Browser SDK.
Two ways to mount it
Auto-mount — put data-key on the <script> tag itself. The script injects an
<avatar-embed> element immediately after itself, copying every data-*
attribute across. The panel appears wherever the script tag sits, so put the tag where
you want the avatar:
<script src="https://www.casola.ai/embed.js" data-key="pk_abc12345.def6789…" data-avatar="mei" data-lang="en" data-name="Mei from Acme"></script>Explicit element — leave data-key off the script tag and write the element
yourself. Loading the script registers the custom element and nothing more, so the
tag can live in <head>, in a bundle, or anywhere else, and the element can be
rendered by your framework whenever you like:
<script src="https://www.casola.ai/embed.js"></script>
<avatar-embed data-key="pk_abc12345.def6789…" data-avatar="mei" data-lang="en" data-name="Mei from Acme"></avatar-embed>Pick one. A script tag that carries data-key and an <avatar-embed> you wrote
yourself gives you two panels, and two panels that both start take two GPU seats.
The element upgrades on insertion and tears down on removal: pull it out of the DOM and the live session is destroyed and its seat released. Rendering it conditionally is safe.
Where the script points
Both the mint (/api/v1/sessions) and the microphone worklet are resolved against the
origin the script was served from, not a compiled-in hostname. A tag loading
https://www.casola.ai/embed.js mints against https://www.casola.ai; the same bundle on a staging domain
mints against staging with no rebuild. Copy the src as-is rather than rehosting the
file — a self-hosted copy would point its mints at your own origin, which serves no API.
Attributes
| Attribute | Required | Default | What it does |
|---|---|---|---|
data-key | yes | — | Publishable key (pk_…). Its registered origins must include the page. |
data-avatar | yes | — | Stock persona id from /api/gallery. |
data-lang | no | en | BCP-47 tag for the conversation language, passed to the renderer. |
data-name | no | — | Display name in the in-session banner (AI · Mei from Acme). Rendered as text. |
data-notice | no | see below | Replaces the line above the Start button. Plain text — markup is escaped, so a link stays literal. |
data-record | no | off | "true" — and only that exact string — opts the session into raw A/V recording. |
data-key, data-avatar, data-lang and data-record are read when Start is
pressed, so changing them beforehand takes effect. data-notice is rendered when the
element mounts; there is no attribute observer, so set it in your markup rather than
patching it later.
data-avatar is not optional
The session token names the exact avatar version the renderer must bind, so a mint
with nothing to resolve is refused with 400 persona_required. The embed catches this
before it reaches the network and shows Missing data-avatar in the panel.
Under the retired v1 wire the attribute was optional and the renderer picked a default. Integrations written against that behaviour need the attribute added.
Recording
Recording is off unless you ask for it. The default session records no microphone
audio, no reply clips, and no whole-session video, and the panel’s banner says so by
omitting the ● REC segment.
data-record | Mint sends | Banner | Captured |
|---|---|---|---|
absent, or anything but "true" | recording: false | AI · Name | Nothing |
"true" | recording: true | ● REC · AI · Name | Microphone audio, reply clips, whole-session recording |
recording is the only capture field the embed sends, in both directions. Full field
semantics: the recording row in
Sessions → Session shape.
Recording is not transcription. The banner discloses live audio and video kept as
media. Turning recording off does not turn off the transcript — speech is still
processed so the avatar can hold a conversation, and
GET /api/v1/sessions/{id}/transcript still
answers. The deprecated observability: false switch does collapse both at once, which
is exactly why the embed does not send it.
Not every renderer in the fleet understands the recording switch — an older generation
predates it and captures unless it is told otherwise through the legacy field. You do
not have to think about that: the platform knows which renderer your session landed on
and translates a recording-off mint into whatever switch that renderer obeys, failing
closed toward suppression when it cannot tell. data-record off means nothing is
captured, whichever renderer answers.
When the opt-in is refused
recording: true is fenced by plan and by data-processing terms. A zero-data-retention
workspace, a workspace whose recording eligibility is switched off, and the anonymous
tier all answer the mint with 403 recording_not_allowed.
The embed does not fail the visitor over it. It re-mints once without recording, so the conversation starts either way, and writes a warning to the browser console for whoever set the attribute:
[avatar-embed] data-record="true" was refused for this key (recording_not_allowed) —starting the session without recording.The banner follows what the mint granted rather than what the attribute asked for, so a
downgraded session shows no ● REC. Check the console before concluding that recording
is on: the attribute is a request, not a guarantee.
Your disclosure duty
Opting in records an identifiable person’s voice. Telling your end users, and collecting whatever consent their jurisdiction requires, is yours to do — several US states require all-party consent to record a conversation, and voice recordings count as biometric data in some places. See Session Capture and Customer Obligations in the Developer Terms.
data-notice is where that line goes. The default notice deliberately says nothing
about recording, because by default there is none:
You are talking to an AI avatar. The face and voice are synthetic.
<avatar-embed data-key="pk_abc12345.def6789…" data-avatar="mei" data-record="true" data-notice="You are talking to an AI avatar. This conversation is recorded so we can improve our support."></avatar-embed>Replacing the notice replaces it entirely, so keep the AI disclosure in your own wording — it is a Customer Obligation independent of recording.
What the visitor sees
Before Start: the notice line and a Start button over a dark panel. The browser is not asked for the microphone until Start is pressed, which is also what unlocks video autoplay.
Once the first frame decodes, the overlay drops away and a small banner sits in the bottom-left corner for the rest of the call:
data-record | data-name | Banner |
|---|---|---|
| off | unset | AI |
| off | Mei from Acme | AI · Mei from Acme |
| on (and granted) | Mei from Acme | ● REC · AI · Mei from Acme |
It is persistent rather than a one-time toast, so that the avatar being an AI — and the
recording, when there is one — stays evident throughout the conversation and not only
before it. The red dot always sits beside the word REC, never carrying the notice on
colour alone, and it stops pulsing under prefers-reduced-motion.
When the session ends, the banner disappears, the overlay returns, and the button reads Restart.
Status copy
The panel narrates itself. These are the strings a visitor can see, so you can recognise them in a support ticket:
| Situation | Panel text |
|---|---|
| Minting | Checking avatar availability… |
| Queued, position known | Waiting for a free line — #3… |
| Queued, position unknown | Waiting for a free line… |
| Connecting | Connecting to your avatar… |
| Renderer warming up | Preparing your avatar… |
| Renderer warm, call opening | Avatar ready. Starting your call… |
| Live | Live |
| Hung up or capped | Session ended |
| Socket died mid-call | Connection error |
Capacity
Sessions run on real GPU seats, so a busy fleet is a state you have to design for. The embed handles it for you: a full fleet queues the visitor and the panel counts their place down. No configuration, and no error unless the wait runs past the admission horizon.
Failures are shown as sentences, never as error codes:
| Cause | Panel text |
|---|---|
fleet_busy, queue_timeout | All sessions are busy right now — please try again in a moment. |
rate_limited | Too many requests — please wait a moment and try again. |
protocol_unavailable | This avatar is temporarily unavailable — please try again later. |
| Network, CORS, or an unregistered origin | Network error — please try again. |
Anything else, quota_exhausted included | Could not start the session — please try again. |
Two of those deserve a note.
protocol_unavailable means the fleet has no renderer speaking protocol v2, which
the embed requires and offers no fallback for. That is a rollout gap on our side rather
than something a retry fixes, which is why its copy says “later” where the busy-fleet
copy says “in a moment”.
queue_timeout is the far end of the queue: the embed waits about six minutes in line
before it gives up, and the visitor sees the same busy sentence as a hard fleet_busy.
A workspace that has spent its monthly minutes falls into the last row, so its visitors are told to try again when the honest answer is “not until the quota resets”. Watch your usage rather than relying on the panel to explain that one.
While a session is live the element heartbeats its seat, and releases it on hang-up, on page unload, and when the element leaves the DOM. A visitor who closes the tab does not strand a seat for the next one.
Styling
The panel lives in a shadow root, so its internals are sealed off from your stylesheet — your CSS cannot reach the button or the banner, and your resets cannot break them. The host element is a plain block you style normally:
avatar-embed { display: block; width: min(100%, 960px); margin-inline: auto;}The panel fills the host’s width at a fixed 16:9 aspect ratio with rounded corners, so setting the width is usually all you need. Give it a real width in a flex or grid parent, which may otherwise collapse it.
Requirements
- A secure context.
https://, orlocalhostfor development. Browsers refuse microphone access anywhere else. - A microphone. The conversation is full-duplex voice; there is no text-only mode in the embed.
- Custom elements and MSE. Current Chrome, Edge, Safari, and Firefox. iOS Safari is handled by the SDK underneath.
When to graduate to the SDK
Reach for a server-side mint and @casola/avatar-client when you
need any of these, all of which a publishable key cannot do:
- A custom avatar — your own face and voice
- A system prompt, backstory, seeded history, or tool calling
- Your own interface: transcripts, captions, a different layout, your own controls
- A session length above the environment default, or profile pinning
The Quickstart walks through the server mint and the browser connect end to end.
Troubleshooting
| Symptom | Cause |
|---|---|
| Nothing renders at all | /widget.js instead of /embed.js, or the auto-mount script tag is missing data-key and no <avatar-embed> element was written. |
Missing data-key | The element has no data-key. With the auto-mount form, check the attribute is on the <script> tag — only data-* attributes are copied across. |
Missing data-avatar | Add the persona id. It is required on protocol v2. |
Network error — please try again. | Usually the page’s origin is not on the publishable key’s allowed list, which fails the preflight. Check the browser’s network tab for the blocked OPTIONS. |
No ● REC despite data-record="true" | The mint refused the opt-in; the console carries the reason. See When the opt-in is refused. |
| Two panels | The script tag carries data-key and you also wrote the element. Remove one. |
| Start does nothing on mobile | Not a secure context — the page must be served over https://. |
Next steps
- Sessions — the mint the embed performs on your behalf, in full
- Authentication — publishable keys, scopes, and origin allowlists
- Quickstart — the server-mint path, for everything above
- Need help? Email support@casola.ai