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.

User types in your own UIYour LLM generates a replyThe reply is sent to the avatar via the APIThe avatar renders it as speech + lip-sync

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 site

Get started in 3 steps

1Create an avatar

Go to "Avatars" → New, pick a look and voice. A default API key is generated automatically.

Create
2Grab key & set allowlist

Open the avatar's Embed Code page to copy its API key and add your site to the domain allowlist.

My avatars
3Copy & embed

Pick a method below and replace YOUR_API_KEY with your own.

Which one should I pick?

Take a look at this table first.

MethodBest forControlStyle isolation
JS SDKSPAs, programmatic control, event subscriptions⭐⭐⭐Medium
Web ComponentVue / React / plain HTML, want simple⭐⭐High (Shadow DOM)
iframeStatic pages, CMS, blog posts, zero depsHighest (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.

A · Text

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.

Pros
  • 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
Trade-offs
  • Voice limited to our Azure catalog; no custom voice cloning
  • Cannot reuse your existing TTS investment
Key APIs
widget.speak(text) // your LLM text → we synthesize TTS + lip-sync
Best for
  • 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
B · Audio

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.

Pros
  • 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
Trade-offs
  • 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
Key APIs
widget.lipsync(audioUrl) // your audio (URL) → we drive lip-sync onlywidget.lipsync(audioBlob) // your audio (Blob) → we drive lip-sync only
Best for
  • Branded IP / celebrity custom-voice avatars
  • Dialect broadcasts (Cantonese / Hokkien / regional English etc.)
  • Existing TTS platform users who only need lip-sync
Still not sure?
  • 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

React · Route A
// 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.

apiKeystringrequired

Authentication credential that authorizes the request.

format: 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.

format: 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.

values
'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 click

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

values
'bottom-right'Bottom-right (default)
'bottom-left'Bottom-left
'top-right'Top-right
'top-left'Top-left

Pass appearance.position; SDK applies fixed left/right + top/bottom to the wrapper

appearance.sizeenumoptionaldefault: 'normal'

Sets the avatar display size.

values
'compact'340 × 55vh — mobile-friendly
'normal'400 × 66vh (default)
'large'480 × 80vh — large screens

Pass appearance.size; SDK sets the matching pixel values on the wrapper at expand time

appearance.scalenumberoptionaldefault: 1

Scales the avatar proportionally.

format: 0.5 ~ 1.5 recommended

Pass appearance.scale: 0.9; SDK applies transform: scale(0.9) to the iframe at panel state

appearance.zIndexnumberoptionaldefault: 2147483647

Controls the avatar stacking order relative to your page elements.

format: Any integer; lower it (e.g. 1) to let host-site elements stack above the avatar

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

values
trueiframe receives pointer events
falseClick-through to your underlying UI

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

format: 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.

format: 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: false

Defers avatar loading until the user opens it, speeding up initial page load.

values
falseinit creates iframe + loads GLB + opens session immediately (default)
trueinit 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>.

Applies to: JS SDK Web Component
Call syntax
SDKconst widget = AIAvatar.init({...}); await widget.speak('hi')
WCawait document.querySelector('ai-avatar').speak('hi')
Return value: SpeakResultThe resolved value of the promises returned by speak() and lipsync(). All fields are optional.
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().
MethodSignatureDescription
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(): booleanSynchronous check of whether the avatar GLB has finished loading.
Example: drive the avatar from your own UI and await the result (JS SDK)

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.

JS SDK
// 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() => void

Fires 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_LOADED
onSpeakingChange(speaking: boolean) => void

Fires 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_SPEAKING
onError(err: { code: string; message: string }) => void

Fires on errors — load failure, drive (speak / lipsync) failure, auth / quota issues.

SDK: onError(err) · WC: avatar-error (e.detail) · iframe: ERROR

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

MethodSignatureDescription
show()show(): voidShow 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(): voidHide the whole widget (avatar + bubble). State is preserved — call show() again to restore.
expand()expand(): voidbubble → full expansion. Only applies when the widget starts as a bubble.
collapse()collapse(): voidfull → bubble collapse. Only applies when the widget has a bubble state.
toggle()toggle(): voidToggle between expand and collapse.
reloadConfig()reloadConfig(): voidRe-fetch the avatar config after you change it in the console (no page reload needed).
destroy()destroy(): voidFully 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.
iframe has no runtime instance — use the equivalents below
  • 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

CodeMeaningWhat to check
400Invalid request parametersCheck required fields against the API doc (text length, voiceId validity, etc).
401Token invalid or disabledCheck the API key for typos or whether it's disabled in the console.
402Quota exhaustedGo to Billing & Usage to upgrade the plan or buy a pack.
403Origin not allowlistedAdd the current origin to the Domain allowlist on the project's Embed Code page.
404Resource not foundVerify sessionId spelling, and whether the resource belongs to your account.
422Text / voice language mismatch, or empty synthesized audioe.g. English text paired with a voice meant for another language. Set the project's voice to match your content language.
429Rate limitedReduce 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.
JS
// 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
Back to home