ContentsHow it works
Getting started
API reference
Advanced
DOCS
Integration Docs
Embed the AI Avatar into your site — 3 ways, minimum 2 lines of code.
How it works — the platform renders, you bring the brain
The platform handles avatar rendering, text-to-speech (TTS) and lip-sync. The conversational brain — your LLM — is wired in from your own system. We bind to no specific model and never own your conversation logic.
Bring-your-own-LLM (BYO-LLM) is the standard architecture for real-time avatars — Anam, Tavus and D-ID all follow it: the platform focuses on real-time rendering while you retain full control of the conversational intelligence. Collect user input in your own UI, call your LLM, then drive the avatar with speak() / lipsync().
See the avatar in a live conversation — try it on our siteGet started in 3 steps
Go to "Avatars" → New, pick a look and voice. A default API key is generated automatically.
Create →Open the avatar's Embed Code page to copy its API key and add your site to the domain allowlist.
My avatars →Pick a method below and replace YOUR_API_KEY with your own.
Which one should I pick?
Take a look at this table first.
| Method | Best for | Control | Style isolation |
|---|---|---|---|
| JS SDK | SPAs, programmatic control, event subscriptions | ⭐⭐⭐ | Medium |
| Web Component | Vue / React / plain HTML, want simple | ⭐⭐ | High (Shadow DOM) |
| iframe | Static pages, CMS, blog posts, zero deps | ⭐ | Highest (sandbox) |
Which integration route should I pick?
Choose based on what your LLM outputs — text only goes A, BYO audio goes B. Both routes can mix in the same widget instance.
Your LLM returns text only
Your backend just sends us the LLM text reply; we synthesize the voice and animate lip-sync. Fastest to integrate — live in 3 lines.
- ✓Fastest to integrate: just return text
- ✓Multi-language out of the box (full Azure TTS catalog)
- ✓Streaming output, first-sentence latency <800ms
- ✓Switch voice from the console — no redeploy needed
- ✗Voice limited to our Azure catalog; no custom voice cloning
- ✗Cannot reuse your existing TTS investment
widget.speak(text) // your LLM text → we synthesize TTS + lip-sync- •General-purpose customer support / FAQ
- •Multi-language scenarios (English / Japanese / Korean / Arabic etc.)
- •MVP rapid validation, with a smooth migration path to Route B later
Your LLM returns text + audio
Your backend generates audio with your own TTS; we only sync the audio to lip motion. Full voice control — ideal for branded experiences.
- ✓Full voice control: branded / celebrity / IP voice
- ✓Supports dialects, custom pronunciation, emotional tones
- ✓Reuse your existing TTS platform investment
- ✓Using audioUrl skips our TTS character quota
- ✗You provide the TTS (you control cost and latency)
- ✗Audio must be WAV container, ≤ 10MB per file
- ✗When using audioUrl, the audio host must allow CORS for our domain
widget.lipsync(audioUrl) // your audio (URL) → we drive lip-sync onlywidget.lipsync(audioBlob) // your audio (Blob) → we drive lip-sync only- •Branded IP / celebrity custom-voice avatars
- •Dialect broadcasts (Cantonese / Hokkien / regional English etc.)
- •Existing TTS platform users who only need lip-sync
- →First time integrating → pick A. Returning a string is enough to make the avatar speak — done in 3 lines.
- →Must use your own voice / celebrity IP → pick B. Your TTS produces audio, we sync the lips.
- →Undecided → ship A first; switching to B later needs no architecture change, just swap the API on the same widget instance.
How to use in React / Vue?
Use our npm package — a real React / Vue <AIAvatar> component with a ref handle for speak() / lipsync(). Install: npm i @ai-avatar/embed-sdk
// Route A: your LLM returns text; we do TTS + lip-sync
// npm i @ai-avatar/embed-sdk
import { useRef, useState } from 'react';
import { AIAvatar, type AvatarInstance } from '@ai-avatar/embed-sdk/react';
export function AvatarCanvas() {
const avatar = useRef<AvatarInstance>(null);
const [ready, setReady] = useState(false);
// Your own input box submits → call YOUR LLM → drive the avatar
async function onUserSend(text: string) {
const reply = await myLLM(text); // your own LLM
const r = await avatar.current?.speak(reply); // platform TTS + lip-sync
// r?.usage?.chars, r?.interrupted ...
}
return (
<>
{/* Your container — the avatar fills it 100%; size / position / radius via your CSS */}
<div id="avatar-box" style={{ width: 360, height: 480, borderRadius: 16, overflow: 'hidden' }} />
<AIAvatar
ref={avatar}
apiKey="YOUR_API_KEY"
mount="#avatar-box" // display-only canvas (passing mount auto-enables inline)
onAvatarLoaded={() => setReady(true)} // enable your input once the avatar is ready
onSpeakingChange={(speaking) => {/* sync your UI */}}
/>
{/* ...your own input box; call onUserSend on submit, disable until ready... */}
</>
);
}
// ↓ Replace with your own LLM call
async function myLLM(text: string) {
const res = await fetch('/api/my-llm', { method: 'POST', body: JSON.stringify({ text }) });
return (await res.json()).reply as string;
}Complete parameter reference
All available parameters + types + defaults + descriptions, switch by integration method.
apiKeystringrequiredAuthentication credential that authorizes the request.
format 'sk-<48 chars>'API key — auto-created with the avatar; copy it from the avatar's Embed Code page. One key per site + an Origin allowlist makes revocation precise when leaked
mountstring | HTMLElementoptionaldefault: —Mounts the avatar as a canvas that fills your own container.
CSS selector or DOM element, e.g. '#ai-avatar'mount: '#ai-avatar' (or pass the element). Required when displayMode is 'inline'
appearance.displayModeenumoptionaldefault: 'bubble'Chooses a canvas that fills your container, or a floating bubble.
'inline'— Talking canvas: the avatar fills your mount container (the recommended display-only pattern). Always on-screen, placed inside your own layout'bubble'— Floating bubble pinned to a viewport corner (default), expands on clickHow the avatar is placed on your page. 'inline' needs a mount container; 'bubble' is the self-managed floating widget
appearance.positionenumoptionaldefault: 'bottom-right'Anchors the widget to a corner of the viewport.
'bottom-right'— Bottom-right (default)'bottom-left'— Bottom-left'top-right'— Top-right'top-left'— Top-leftPass appearance.position; SDK applies fixed left/right + top/bottom to the wrapper
appearance.sizeenumoptionaldefault: 'normal'Sets the avatar display size.
'compact'— 340 × 55vh — mobile-friendly'normal'— 400 × 66vh (default)'large'— 480 × 80vh — large screensPass appearance.size; SDK sets the matching pixel values on the wrapper at expand time
appearance.scalenumberoptionaldefault: 1Scales the avatar proportionally.
0.5 ~ 1.5 recommendedPass appearance.scale: 0.9; SDK applies transform: scale(0.9) to the iframe at panel state
appearance.zIndexnumberoptionaldefault: 2147483647Controls the avatar stacking order relative to your page elements.
Any integer; lower it (e.g. 1) to let host-site elements stack above the avatarPass appearance.zIndex: 1; SDK applies it to the wrapper
appearance.interactivebooleanoptionaldefault: false (pointer events pass through by default)Determines whether the avatar captures pointer events or lets them pass through.
true— iframe receives pointer eventsfalse— Click-through to your underlying UIPass appearance.interactive: false; SDK sets pointer-events: none on the wrapper
appearance.left / right / top / bottomnumber | stringoptionaldefault: —Positions the widget with pixel-level offsets for fine control.
number auto-appends px (24 → '24px'); string passes through ('10vw' / '5%' / '50px')Pass appearance.left: 24 etc; SDK writes only the sides you specified to the wrapper inline style (others stay auto)
appearance.width / heightnumber | stringoptionaldefault: —Overrides the size preset with explicit pixel dimensions.
number auto-appends px (320 → '320px'); string passes through ('40vw' / '60%' / '500px')Pass appearance.width / height; SDK writes it to the wrapper inline style at expand
behavior.lazyLoadbooleanoptionaldefault: falseDefers avatar loading until the user opens it, speeding up initial page load.
false— init creates iframe + loads GLB + opens session immediately (default)true— init only places a wrapper; defers iframe creation until widget.show()Pass behavior.lazyLoad: true; init only places a wrapper. widget.show() triggers the actual GLB download + session creation
Runtime control API
Once loaded, drive the avatar programmatically through instance methods: call them on the instance returned by init() (SDK) or on the <ai-avatar> element directly (Web Component). speak() and lipsync() are asynchronous and return Promise<SpeakResult>.
const widget = AIAvatar.init({...}); await widget.speak('hi')await document.querySelector('ai-avatar').speak('hi')audioDurationnumber- Duration of the spoken audio, in seconds.
usage{ chars?: number; seconds?: number }- Metered usage for the call: synthesized characters (chars) and audio seconds (seconds).
interruptedboolean- True when the playback was interrupted by a newer drive call or stop().
| Method | Signature | Description |
|---|---|---|
speak() | speak(text, options?): Promise<SpeakResult> | Route A: send text, the platform synthesizes TTS and drives lip-sync. The Promise resolves when the whole utterance finishes (or is interrupted), and rejects on failure. await it to read SpeakResult (duration + usage). |
lipsync() | lipsync(audio, text?): Promise<SpeakResult> | Route B: send audio (WAV URL, or Blob / File ≤ 10MB), the platform drives lip-sync only (no TTS). Same Promise<SpeakResult> semantics as speak(). |
waitAvatarLoaded() | waitAvatarLoaded(): Promise<void> | Resolves once the avatar GLB + lip-sync engine are ready (resolves immediately if already loaded). Display-only clients await this before enabling their own input box. |
isAvatarLoaded() | isAvatarLoaded(): boolean | Synchronous check of whether the avatar GLB has finished loading. |
Display-only pattern — you own the input box + LLM, the avatar is just a canvas. speak() / lipsync() resolve when the avatar finishes; read SpeakResult for usage, or interrupted when a newer call cuts it off.
// HTML: <div id="ai-avatar" style="width:360px;height:480px;border-radius:16px;overflow:hidden"></div>
const avatar = AIAvatar.init({
apiKey: 'sk-...',
mount: '#ai-avatar', // your container; the avatar fills it 100%
appearance: { displayMode: 'inline' }, // display-only canvas
onAvatarLoaded() { /* avatar ready — enable your input box */ },
onSpeakingChange(speaking) { /* sync your own UI while it speaks */ },
});
// Or await readiness instead of the callback
await avatar.waitAvatarLoaded();
// Your own input box submits → call YOUR LLM → drive the avatar
async function onUserSend(text) {
const reply = await myLLM(text); // your own LLM
const result = await avatar.speak(reply); // platform TTS + lip-sync
if (!result.interrupted) {
console.log('done, TTS chars used:', result.usage?.chars);
}
}Callbacks / events
Pass these to init() as callbacks (SDK), or listen as events — Web Component avatar-* CustomEvents / iframe postMessage AVATAR_*.
onAvatarLoaded() => voidFires once the avatar GLB + lip-sync engine are ready. Display-only clients enable their own input box here.
SDK: onAvatarLoaded() · WC: avatar-loaded · iframe: AVATAR_LOADEDonSpeakingChange(speaking: boolean) => voidFires when the avatar starts / stops speaking. Sync your own UI — e.g. disable the input box while it speaks.
SDK: onSpeakingChange(speaking) · WC: avatar-speaking (e.detail.speaking) · iframe: AVATAR_SPEAKINGonError(err: { code: string; message: string }) => voidFires on errors — load failure, drive (speak / lipsync) failure, auth / quota issues.
SDK: onError(err) · WC: avatar-error (e.detail) · iframe: ERRORWidget control
Control the widget's visibility, shape, and lifecycle. show() / hide() toggle overall visibility, while expand() / collapse() switch between the bubble and full states — two independent dimensions.
| Method | Signature | Description |
|---|---|---|
show() | show(): void | Show the widget. In lazyLoad mode, the first call triggers iframe creation + GLB download + session creation; subsequent calls just toggle visibility without re-downloading. |
hide() | hide(): void | Hide the whole widget (avatar + bubble). State is preserved — call show() again to restore. |
expand() | expand(): void | bubble → full expansion. Only applies when the widget starts as a bubble. |
collapse() | collapse(): void | full → bubble collapse. Only applies when the widget has a bubble state. |
toggle() | toggle(): void | Toggle between expand and collapse. |
reloadConfig() | reloadConfig(): void | Re-fetch the avatar config after you change it in the console (no page reload needed). |
destroy() | destroy(): void | Fully tear down (remove DOM + unbind events). Not reversible. Web Component also removes the <ai-avatar> element itself from DOM; for SDK, call AIAvatar.init() again to re-create. |
- •speak (text→TTS+lipsync): postMessage `{ __avatar: true, id: String(Date.now()), type: 'SPEAK_TEXT', payload: { text } }` to iframe.contentWindow
- •lipsync (audio→lipsync): postMessage `{ __avatar: true, id, type: 'SPEAK_AUDIO', payload: { audioUrl, text } }` (URL needs CORS for the embed origin) or `payload: { audio, text }` (audio is a Blob/File, passed cross-origin)
- •for both, listen for the `SPEAK_RESULT` message back (correlated by the id you sent) to get the SpeakResult
- •show / hide: flip iframe.style.display = 'block' / 'none' on the host page
- •expand / collapse: postMessage `{ __avatar: true, type: 'EXPAND' / 'COLLAPSE' }` to iframe.contentWindow, paired with the HEIGHT_CHANGE bridge to receive the new size from PureJS
- •destroy: iframe.remove()
Common error codes
| Code | Meaning | What to check |
|---|---|---|
400 | Invalid request parameters | Check required fields against the API doc (text length, voiceId validity, etc). |
401 | Token invalid or disabled | Check the API key for typos or whether it's disabled in the console. |
402 | Quota exhausted | Go to Billing & Usage to upgrade the plan or buy a pack. |
403 | Origin not allowlisted | Add the current origin to the Domain allowlist on the project's Embed Code page. |
404 | Resource not found | Verify sessionId spelling, and whether the resource belongs to your account. |
422 | Text / voice language mismatch, or empty synthesized audio | e.g. English text paired with a voice meant for another language. Set the project's voice to match your content language. |
429 | Rate limited | Reduce client-side call rate or upgrade the plan for higher limits. |
LLM failure fallback — don't leave the avatar spinning
The avatar only speaks what you pass back. If your LLM times out or fails, handle it in your own UI — otherwise the avatar is left with no reply and the user just waits.
- On timeout / error, call speak('Sorry, something went wrong') so the avatar speaks a fallback line instead of going silent.
- Rapid-fire questions: speak / lipsync auto-interrupt the previous playback, but your own UI must dedupe / queue messages itself.
- Gate user input until onAvatarLoaded / avatar-loaded fires — otherwise the first reply may play audio before the avatar appears, with no lip-sync.
// your input handler → your LLM → drive the avatar
async function onUserSend(text) {
try {
const reply = await myCustomerLLM(text);
widget.speak(reply);
} catch (e) {
widget.speak('Sorry, I hit a problem — please try again'); // fallback reply
}
}Security: always configure origin allowlist
Frontend embedding inevitably exposes the API key to browsers — an allowlist is the most effective anti-abuse measure.
Open a project's Embed Code page, find the Domain allowlist section, and add allowed origins (e.g. https://your-site.com). The backend validates the request's Origin header and rejects non-allowlisted origins.
- Exact match (including protocol and port) and *.example.com wildcards for subdomains; one per line
- Add http://localhost:3000 separately for dev — avoid wildcards
- One key per site makes revocation precise on leak