
Liveavatar Integrate
- 677 installs
- 5 repo stars
- Updated July 30, 2026
- heygen-com/liveavatar-agent-skills
LiveAvatar Integrate is a HeyGen agent skill that assesses a developer's tech stack, recommends Embed, FULL, or LITE LiveAvatar integration paths, and guides step-by-step implementation.
About
LiveAvatar Integrate is an end-to-end integration skill from heygen-com/liveavatar-agent-skills for adding HeyGen LiveAvatar to applications. It evaluates the existing tech stack, recommends the optimal path among Embed, FULL, or LITE modes, and walks through implementation step by step. Developers reach for it when shipping conversational or streaming avatar experiences and need structured guidance instead of reading scattered API docs alone. The skill targets integration architecture and rollout rather than generic frontend polish or unrelated video editing.
- Assesses tech stack and recommends best integration path
- Covers Embed, FULL, and LITE integration modes
- Guides end-to-end implementation for first-attempt success
- Requires LiveAvatar API key from dashboard
- Works with Claude Code and skills-compatible agents
Liveavatar Integrate by the numbers
- 677 all-time installs (skills.sh)
- +44 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,450 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/heygen-com/liveavatar-agent-skills --skill liveavatar-integrateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 677 |
|---|---|
| repo stars | ★ 5 |
| Last updated | July 30, 2026 |
| Repository | heygen-com/liveavatar-agent-skills ↗ |
How do you integrate HeyGen LiveAvatar into an app?
End-to-end LiveAvatar integration developer that assesses your tech stack, recommends the optimal integration path (Embed, FULL, or LITE), and guides implementation step by step.
Who is it for?
Developers adding HeyGen LiveAvatar to web or agent products who need a guided integration path matched to their stack.
Skip if: Static video generation or avatar design tasks that do not require LiveAvatar runtime integration.
When should I use this skill?
Task involves HeyGen LiveAvatar setup, choosing Embed vs FULL vs LITE modes, or step-by-step avatar streaming integration.
What you get
Selected Embed, FULL, or LITE integration plan, implementation steps, and wired LiveAvatar streaming setup.
- integration path recommendation
- step-by-step setup guide
- wired avatar stream
Files
LiveAvatar Integration
LiveAvatar gives your product a human face — real-time, lip-synced video avatars that speak, react, and maintain eye contact. This skill assesses what you have, recommends the best integration path, and walks you through building it.
Step 1: Discover What the User Has
Before recommending a path, gather context. Check the codebase and conversation for signals. Do not ask questions the codebase already answers.
Signals to look for in the codebase
Scan for these automatically — do not ask the user if you can detect them:
| Signal | Where to look | What it means |
|---|---|---|
| OpenAI / Anthropic / LLM SDK imports | package.json, requirements.txt, imports | User has their own LLM |
| ElevenLabs / PlayHT / Deepgram TTS SDK | dependencies, imports | User has their own TTS |
| Deepgram / Whisper / AssemblyAI STT SDK | dependencies, imports | User has their own STT |
LiveKit SDK (livekit-server-sdk, @livekit/) | dependencies | User has LiveKit infra |
| Agora SDK | dependencies | User has Agora infra |
| Pipecat imports | dependencies, imports | User has a Pipecat pipeline |
| ElevenLabs Agent / Conversational AI | dependencies, config | User has an ElevenLabs agent |
HEYGEN_API_KEY / LIVEAVATAR_API_KEY | .env, config files | User already has an API key |
| Existing LiveAvatar code | imports, API calls to api.liveavatar.com | Existing integration (debug, not new setup) |
| No backend / static site | file structure (pure HTML/CSS/JS, no server) | Embed is the only option |
Questions to ask (only what's still unknown)
If the codebase scan leaves gaps, ask the user. Frame as a concise checklist — do not ask these one at a time:
To recommend the best LiveAvatar integration for your setup, I need to know:
1. **What's the goal?** (e.g., customer support avatar, sales demo, onboarding guide, talking head on landing page)
2. **Do you have your own AI pipeline?** (STT, LLM, TTS — or any combination)
3. **Do you need programmatic control** over the conversation (events, interrupts, custom logic), or just an avatar on a page?Skip any question the codebase or conversation already answered.
Step 2: Route to the Golden Pathway
Based on what you've gathered, match to ONE pathway. Always pick the simplest path that works. Do not offer multiple options — make the call.
Decision tree
Has NO backend OR just wants an avatar on a page?
→ EMBED
Has NO existing AI stack (no STT, no LLM, no TTS)?
→ FULL MODE (standard)
Has their OWN LLM but no STT/TTS?
→ FULL MODE + Custom LLM
Has their OWN LLM + their own ElevenLabs TTS?
→ FULL MODE + Custom LLM + Custom TTS
Needs explicit mic control (walkie-talkie style)?
→ FULL MODE + Push-to-Talk
Has a COMPLETE pipeline (STT + LLM + TTS)?
→ LITE MODE
Has an ElevenLabs Conversational AI agent?
→ LITE MODE + ElevenLabs Plugin
Has their own LiveKit or Agora infrastructure?
→ LITE MODE + BYO WebRTCGolden pathways (pick one, then implement)
| Pathway | When | Implementation guide |
|---|---|---|
| Embed | No backend, or no custom logic needed | references/embed-guide.md |
| FULL standard | No existing AI stack | references/full-mode-guide.md |
| FULL + Custom LLM | Has own LLM, wants LiveAvatar's ASR + TTS | references/full-mode-guide.md (Custom LLM section) |
| FULL + Custom TTS | Has own ElevenLabs voice | references/full-mode-guide.md (Custom TTS section) |
| FULL + Push-to-Talk | Needs explicit mic control | references/full-mode-guide.md (Push-to-Talk section) |
| LITE standard | Has complete STT + LLM + TTS pipeline | references/lite-mode-guide.md |
| LITE + ElevenLabs Plugin | Has ElevenLabs Conversational AI agent | references/lite-mode-guide.md (ElevenLabs Plugin section) |
| LITE + BYO WebRTC | Has own LiveKit / Agora | references/lite-mode-guide.md (BYO WebRTC section) |
Step 3: Present the Recommendation
Once you've picked a pathway, tell the user what you recommend and why, in 2-3 sentences. Example:
Based on your setup, I recommend FULL Mode with Custom LLM. You already have an OpenAI integration for your LLM, so we'll plug that in and let LiveAvatar handle ASR, TTS, and video. This gets you a conversational avatar without rebuilding your audio pipeline.
Then proceed directly to implementation using the corresponding guide in references/.
Step 4: Implement
Read the appropriate reference guide and implement. Every guide follows the same structure:
1. Prerequisites — what to create/gather before writing code 2. Session lifecycle — step-by-step with curl commands and code 3. Events — what to send and receive 4. Add-ons — mode-specific optional features 5. Sandbox testing — free testing before going live 6. Gotchas — what breaks and how to avoid it
Principles that apply to ALL paths
Backend / frontend split is non-negotiable. X-API-KEY is a secret — backend only. Frontend only gets livekit_client_token (safe for browsers). If you see the API key in client code, stop and restructure.
Context makes the avatar conversational. In FULL Mode, no context_id = silent avatar. No error thrown. Always create a context first, even a minimal "You are a helpful assistant.".
FULL and LITE are completely different protocols. FULL = LiveKit data channels (avatar.* / user.*). LITE = WebSocket (agent.* / session.*). Never mix them.
Start with sandbox. is_sandbox: true, avatar ID dd73ea75-1218-4ef3-92ce-606d5f7fbc0a. Free, ~1 min sessions. Swap to production avatar when ready.
LITE Mode: Fitting into an existing pipeline
LITE users almost always have a working conversational system already. Do not ask them to rebuild their pipeline. Instead, map their existing components onto the LITE turn cycle:
1. Identify their current flow. Read their code to understand how conversation turns work today — where does user audio come in, how does it reach the LLM, how does TTS output get delivered? Look for their event loop, message handler, or turn manager. 2. Find the integration points. You need to hook into three moments in their existing flow:
- User starts/stops speaking → add
agent.start_listening/agent.stop_listening - TTS produces audio → route PCM output to
agent.speakchunks over WebSocket instead of (or in addition to) their current audio output - Response finishes → send
agent.speak_endand wait foragent.speak_ended
3. Adapt, don't replace. If they have a working turn manager, add LiveAvatar calls into it. If they stream TTS to a browser via WebSocket already, tap into that same stream. The goal is the minimum change to get avatar video synced to their existing audio flow. 4. Verify audio format last. Once the wiring is in place, confirm their TTS outputs PCM 16-bit 24KHz. If not, either configure the TTS provider's output format or add resampling at the integration point.
Step 5: Collect Feedback
Trigger this immediately after you finish writing the integration code — do not wait for the user to confirm it works or say they're done. Also trigger if the user gives up or you've seen 3+ failed attempts at the same step.
What to do: Follow the liveavatar-feedback skill. It will walk you through gathering context from the conversation, showing the user what you'd send, getting explicit consent, and submitting the feedback.
Keep it lightweight. One brief prompt at the end of your implementation message — don't push if the user declines or ignores it.
What to consult
- references/embed-guide.md — Embed implementation
- references/full-mode-guide.md — FULL Mode implementation + events + add-ons
- references/lite-mode-guide.md — LITE Mode implementation + events + audio format + add-ons
Embed Implementation Guide
One API call, one iframe. No SDK, no WebRTC, no event handling.
Prerequisites
- LiveAvatar API key (get one at https://app.liveavatar.com)
- An
avatar_id— browse on the dashboard orGET /v1/avatars - A
context_id— create viaPOST /v1/contextswith apromptfield
Implementation
1. Create the embed (BACKEND — X-API-KEY)
curl -X POST https://api.liveavatar.com/v2/embeddings \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"avatar_id": "<avatar_id>",
"context_id": "<context_id>"
}'Response:
{
"code": 1000,
"data": {
"url": "https://embed.liveavatar.com/v1/<id>",
"script": "<iframe src=\"https://embed.liveavatar.com/v1/<id>\" allow=\"microphone\" title=\"LiveAvatar Embed\" style=\"aspect-ratio: 16/9;\"></iframe>"
}
}2. Drop into HTML (FRONTEND)
<iframe
src="https://embed.liveavatar.com/v1/<id>"
allow="microphone"
title="LiveAvatar Embed"
style="aspect-ratio: 16/9; width: 100%; border: none;"
></iframe>Done.
Sandbox testing
Use "is_sandbox": true with avatar ID 65f9e3c9-d48b-4118-b73a-4ae2e3cbb8f0:
{
"avatar_id": "65f9e3c9-d48b-4118-b73a-4ae2e3cbb8f0",
"context_id": "<context_id>",
"is_sandbox": true
}No credits consumed. Note: embed sandbox avatar ID (65f9e3c9-...) differs from session sandbox avatar ID (dd73ea75-...).
Gotchas
1. `allow="microphone"` is required on the iframe. Without it, the browser blocks mic access and the avatar can't hear. 2. API key must stay on the backend. The iframe URL is safe for clients; the API call that creates it is not. 3. No `context_id` = non-conversational avatar. The avatar may not respond to speech without a context.
FULL Mode Implementation Guide
LiveAvatar manages ASR + LLM + TTS + WebRTC. You configure, ship, and handle events. 2 credits/minute.
User audio --> [LiveAvatar: ASR -> LLM -> TTS -> Video] --> Avatar streamPrerequisites
1. API key — https://app.liveavatar.com. Never expose to frontend. 2. Context ID — create one below. Without a context, the avatar is silent (no error thrown). 3. Avatar ID — dashboard or GET /v1/avatars 4. Voice ID — optional for video avatars, required for image avatars. GET /v1/voices
Create a context (required)
curl -X POST https://api.liveavatar.com/v1/contexts \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Agent",
"prompt": "You are a friendly customer support agent for Acme Corp. Help users with billing, account setup, and troubleshooting. Keep responses concise and helpful.",
"opening_text": "Hi there! How can I help you today?"
}'| Field | Required | Description |
|---|---|---|
name | Yes | Identifier for this context |
prompt | Yes | System instructions — role, tone, knowledge boundaries, guardrails |
opening_text | Yes | Greeting spoken when the session starts |
links | No | Array of {"url": "...", "faq": "..."} — URLs the avatar can reference |
Returns: { "data": { "id": "<context_id>", "name": "...", ... } }
Use the returned id as context_id in your session token. Contexts are reusable — create once, reference in many sessions.
Session Lifecycle
Step 1: Create session token (BACKEND, X-API-KEY)
curl -X POST https://api.liveavatar.com/v1/sessions/token \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"mode": "FULL",
"avatar_id": "<avatar_id>",
"avatar_persona": {
"voice_id": "<voice_id>",
"context_id": "<context_id>",
"language": "en"
}
}'Returns: { "data": { "session_id": "...", "session_token": "..." } }
Step 2: Start session (BACKEND, Bearer <session_token>)
curl -X POST https://api.liveavatar.com/v1/sessions/start \
-H "Authorization: Bearer <session_token>"Use the session token, NOT the API key. This is the most common auth mistake.
Returns: { "data": { "livekit_url": "wss://...", "livekit_client_token": "..." } }
Step 3: Join LiveKit room (FRONTEND)
Pass livekit_url + livekit_client_token to your frontend.
Quick test (no frontend code):
https://meet.livekit.io/custom?liveKitUrl=<livekit_url>&token=<livekit_client_token>Production — Web SDK:
npm install @heygen/liveavatar-web-sdkFor frontend implementation, clone or read the Web SDK repo: https://github.com/heygen-com/liveavatar-web-sdk
The repo contains:
packages/js-sdk/— the SDK source with full TypeScript types (LiveAvatarSession,SessionEvent,AgentEventsEnum,VoiceChat, etc.)apps/demo/— a complete Next.js demo app showing FULL Mode, LITE Mode, and Push-to-Talk
Key patterns from the demo app: 1. Backend API route creates session token and returns session_token to the client 2. Frontend creates new LiveAvatarSession(sessionToken) — the SDK handles /v1/sessions/start and LiveKit connection internally 3. Listen for SessionEvent.SESSION_STREAM_READY, then call session.attach(videoElement) to render 4. Call session.voiceChat.start() to enable microphone 5. Use session.message(text) (LLM response) or session.repeat(text) (verbatim) to make the avatar speak 6. Call session.stop() to end
Auth split
| What | Where | Auth |
|---|---|---|
| Create session token | Backend | X-API-KEY (secret) |
| Start session | Backend | Bearer <session_token> |
| Join LiveKit room | Frontend | livekit_client_token (safe) |
| Send/receive events | Frontend | LiveKit data channels |
| Stop session | Backend | Bearer <session_token> |
Event System: LiveKit Data Channels
Two topics: agent-control (send) and agent-response (receive).
Commands (to agent-control)
| Event | Payload | What it does |
|---|---|---|
avatar.interrupt | none | Stop avatar mid-speech, clear queue |
avatar.speak_text | {"text": "..."} | Avatar speaks text verbatim |
avatar.speak_response | {"text": "..."} | Avatar generates LLM response, then speaks |
avatar.start_listening | none | Switch to listening state |
avatar.stop_listening | none | Switch to idle state |
Events (from agent-response)
| Event | Payload | What it means |
|---|---|---|
user.transcription | {"text": "..."} | What the user said |
avatar.transcription | {"text": "..."} | What the avatar is saying |
avatar.speak_started | none | Avatar started speaking |
avatar.speak_ended | none | Avatar finished speaking |
user.speak_started | none | User started talking |
user.speak_ended | none | User stopped talking |
session.stopped | {"end_reason": "..."} | Session ended |
All events: { "event_id": "string", "event_type": "string", "session_id": "string", "source_event_id": "string | null" }
Session Management
- Keep-alive:
POST /v1/sessions/keep-alivewithBearer <session_token>every 2-3 minutes (5-min timeout) - Stop:
POST /v1/sessions/stopwithBearer <session_token> - Video quality:
video_quality(very_high/high/medium/low) +video_encoding(VP8/H264) in session token
Custom LLM (add-on)
Use your own OpenAI-compatible LLM.
Setup
1. Store API key:
curl -X POST https://api.liveavatar.com/v1/secrets \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"secret_type": "LLM_API_KEY", "secret_value": "<your_llm_key>", "secret_name": "My LLM Key"}'2. Create LLM config:
curl -X POST https://api.liveavatar.com/v1/llm-configurations \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o", "secret_id": "<secret_id>", "base_url": "https://api.openai.com/v1"}'3. Add to session token:
{
"mode": "FULL",
"avatar_id": "<avatar_id>",
"llm_configuration_id": "<config_id>",
"avatar_persona": { "context_id": "<context_id>", "language": "en" }
}Works with any OpenAI-compatible endpoint (OpenAI, Azure, Anthropic via proxy, local models). base_url defaults to OpenAI if omitted.
Custom TTS (add-on)
Use your own ElevenLabs voice.
Setup
1. Store ElevenLabs key:
curl -X POST https://api.liveavatar.com/v1/secrets \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"secret_type": "ELEVENLABS_API_KEY", "secret_value": "<elevenlabs_key>", "secret_name": "ElevenLabs Key"}'2. Import voice:
curl -X POST https://api.liveavatar.com/v1/voices/third_party \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"secret_id": "<secret_id>", "voice_id": "<elevenlabs_voice_id>"}'3. Use the returned LiveAvatar `voice_id` (not the ElevenLabs ID) in avatar_persona.voice_id.
Push-to-Talk (add-on)
Set interactivity_type: "PUSH_TO_TALK" in the session token. Then send:
user.start_push_to_talk— begin capturing audiouser.stop_push_to_talk— stop capturing audio
Audio outside PTT windows is ignored.
Sandbox
{
"mode": "FULL",
"is_sandbox": true,
"avatar_id": "dd73ea75-1218-4ef3-92ce-606d5f7fbc0a",
"avatar_persona": { "voice_id": "<voice_id>", "language": "en" }
}~1 min sessions, no credits.
Gotchas
1. No `context_id` = silent avatar. Streams video, ignores speech. No error. #1 failure cause. 2. Wrong auth on `/sessions/start`. Use Bearer <session_token>, not X-API-KEY. 3. API key in frontend. Must never leave your backend. 4. Image avatars have no voice. Must specify voice_id or session fails. 5. 5-minute timeout. Send keep-alive every 2-3 minutes. 6. Deleting secrets breaks dependents. No cascade warning. Rotate: new → update → delete old.
LITE Mode Implementation Guide
You bring your own AI stack (STT + LLM + TTS). LiveAvatar only renders video from your audio. 1 credit/minute.
User audio --> [Your stack: ASR -> LLM -> TTS] --> audio --> [LiveAvatar: Video] --> Avatar streamLITE Mode was formerly called "Custom Mode."
Prerequisites
1. API key — https://app.liveavatar.com 2. Avatar ID — dashboard or GET /v1/avatars 3. Your TTS must output PCM 16-bit, 24KHz — wrong format = garbled avatar with NO error
Session Lifecycle
Step 1: Create session token (BACKEND, X-API-KEY)
curl -X POST https://api.liveavatar.com/v1/sessions/token \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"mode": "LITE", "avatar_id": "<avatar_id>"}'Returns: { "data": { "session_id": "...", "session_token": "..." } }
Step 2: Start session (BACKEND, Bearer <session_token>)
curl -X POST https://api.liveavatar.com/v1/sessions/start \
-H "Authorization: Bearer <session_token>"Returns three things:
{
"data": {
"livekit_url": "wss://...",
"livekit_client_token": "...",
"ws_url": "wss://..."
}
}livekit_url+livekit_client_token→ frontend (video stream)ws_url→ your backend/agent (audio commands via WebSocket)
Step 3: Connect both
Frontend — join LiveKit room for video using the Web SDK:
npm install @heygen/liveavatar-web-sdkThe SDK handles LiveKit room connection internally — create new LiveAvatarSession(sessionToken), listen for SESSION_STREAM_READY, then call session.attach(videoElement). For LITE Mode, the frontend is video-only — your backend handles all audio via WebSocket.
For frontend implementation details, clone or read the Web SDK repo: https://github.com/heygen-com/liveavatar-web-sdk
The apps/demo/ directory has a complete Next.js example including LITE Mode.
Or quick test: https://meet.livekit.io/custom?liveKitUrl=<livekit_url>&token=<livekit_client_token>
Backend/Agent — connect WebSocket to ws_url for audio commands.
Event System: WebSocket
LITE uses a WebSocket, NOT LiveKit data channels. Completely different from FULL Mode.
CRITICAL: Wait for "connected"
After connecting to ws_url, wait for:
{"type": "session.state_updated", "state": "connected"}Events sent before `connected` are silently dropped. No error.
Commands you send
| Event | Format |
|---|---|
agent.speak | {"type": "agent.speak", "event_id": "<id>", "audio": "<base64-pcm>"} |
agent.speak_end | {"type": "agent.speak_end", "event_id": "<id>"} |
agent.interrupt | {"type": "agent.interrupt"} |
agent.start_listening | {"type": "agent.start_listening", "event_id": "<id>"} |
agent.stop_listening | {"type": "agent.stop_listening", "event_id": "<id>"} |
session.keep_alive | {"type": "session.keep_alive", "event_id": "<id>"} |
`agent.speak_end` is required after sending audio. Without it, the avatar won't transition to idle/listening.
`agent.start_listening` / `agent.stop_listening` control the avatar's visual listening state. When listening is active, the avatar appears attentive — nodding, maintaining eye contact, showing engaged body language. When stopped, the avatar returns to a neutral idle pose. Use start_listening when the user begins speaking and stop_listening when they finish, so the avatar reacts naturally to conversation turns.
Events you receive
| Event | Payload |
|---|---|
session.state_updated | `{"state": "connected" \ |
agent.speak_started | {"event_id": "...", "task": {"id": "..."}} |
agent.speak_ended | {"event_id": "...", "task": {"id": "..."}} |
Conversation Turn Orchestration
For conversational use cases, follow this turn cycle to keep the avatar's visual state in sync with the conversation:
┌─────────────────────────────────────────────────────────┐
│ 1. User starts speaking │
│ → send agent.start_listening │
│ → avatar becomes attentive (nods, eye contact) │
│ │
│ 2. User finishes speaking │
│ → send agent.stop_listening │
│ → run your pipeline: STT → LLM → TTS │
│ │
│ 3. Stream TTS audio │
│ → send agent.speak chunks (same event_id) │
│ → send agent.speak_end when stream finishes │
│ → avatar lip-syncs to audio │
│ │
│ 4. Avatar finishes speaking │
│ → receive agent.speak_ended from server │
│ → send agent.start_listening │
│ → ready for next turn │
│ │
│ ✕ User interrupts mid-speech │
│ → stop your send loop │
│ → send agent.interrupt │
│ → send agent.start_listening │
│ → go to step 2 when user finishes │
└─────────────────────────────────────────────────────────┘# Pseudocode — full conversation loop
while session_active:
start_listening(ws)
user_audio = wait_for_user_speech() # Your VAD / STT
stop_listening(ws)
text = transcribe(user_audio) # Your STT
response = llm_generate(text) # Your LLM
tts_stream = tts_synthesize(response) # Your TTS (PCM 24KHz)
stream_audio(ws, tts_stream) # See "Streaming TTS audio" below
wait_for_event(ws, "agent.speak_ended") # Avatar done speakingThis is the recommended pattern for any conversational LITE integration. The key is always bookending the user's turn with start_listening / stop_listening so the avatar visually reacts, and always waiting for agent.speak_ended before starting the next listening cycle.
Audio Format
This is where most LITE integrations break.
| Parameter | Required value |
|---|---|
| Format | PCM (raw bytes, no container/headers) |
| Bit depth | 16-bit signed (little-endian) |
| Sample rate | 24,000 Hz |
| Channels | Mono |
| Encoding | Base64 |
| Chunk size | 600ms first chunk, 1s subsequent |
| Max per packet | 1 MB |
Wrong sample rate = garbled or silent avatar. No error returned.
Best practice: configure your TTS provider to output PCM 24KHz directly. Most providers (ElevenLabs, OpenAI, Google, Azure) have an output format / sample rate setting. This avoids resampling entirely. For example, ElevenLabs lets you set output_format: "pcm_24000" in the API request.
Python: Send audio
import base64, json
def send_audio(ws, pcm_bytes_24khz, event_id):
ws.send(json.dumps({
"type": "agent.speak",
"event_id": event_id,
"audio": base64.b64encode(pcm_bytes_24khz).decode()
}))
ws.send(json.dumps({
"type": "agent.speak_end",
"event_id": event_id
}))Python: Resample to 24KHz
import numpy as np
def resample_to_24k(pcm_bytes: bytes, original_rate: int) -> bytes:
samples = np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float64)
new_length = int(len(samples) * 24000 / original_rate)
indices = np.linspace(0, len(samples) - 1, new_length)
resampled = np.interp(indices, np.arange(len(samples)), samples)
return np.clip(resampled, -32768, 32767).astype(np.int16).tobytes()Node.js: Send audio
function sendAudio(ws, pcmBuffer) {
const eventId = `speak-${Date.now()}`;
ws.send(JSON.stringify({
type: 'agent.speak', event_id: eventId,
audio: pcmBuffer.toString('base64')
}));
ws.send(JSON.stringify({
type: 'agent.speak_end', event_id: eventId
}));
}Streaming TTS audio
When streaming TTS output, send chunks under the same `event_id` as they arrive. Use a larger first chunk (600ms) to let the avatar buffer, then 1s chunks after that. Send agent.speak_end once after the stream ends.
import base64, json
from uuid import uuid4
BYTES_PER_SEC = 48_000 # 24KHz × 16-bit mono = 48,000 bytes/sec
FIRST_CHUNK = int(BYTES_PER_SEC * 0.6) # 600ms — initial buffer
NEXT_CHUNK = BYTES_PER_SEC # 1s — subsequent chunks
def stream_audio(ws, tts_stream):
"""Stream TTS audio chunks to LiveAvatar as they arrive."""
event_id = f"speak-{uuid4()}"
buffer = b""
first = True
for pcm_data in tts_stream:
buffer += pcm_data
target = FIRST_CHUNK if first else NEXT_CHUNK
while len(buffer) >= target:
chunk, buffer = buffer[:target], buffer[target:]
ws.send(json.dumps({
"type": "agent.speak",
"event_id": event_id,
"audio": base64.b64encode(chunk).decode()
}))
first = False
target = NEXT_CHUNK
# Flush remaining audio
if buffer:
ws.send(json.dumps({
"type": "agent.speak",
"event_id": event_id,
"audio": base64.b64encode(buffer).decode()
}))
ws.send(json.dumps({
"type": "agent.speak_end",
"event_id": event_id
}))
# Server returns agent.speak_ended when the avatar finishes speakingInterrupting during a stream
If the user speaks while you're streaming audio, stop sending chunks and send agent.interrupt:
import threading
streaming = threading.Event()
def stream_audio(ws, tts_stream):
event_id = f"speak-{uuid4()}"
streaming.set()
buffer = b""
first = True
for pcm_data in tts_stream:
if not streaming.is_set():
break
buffer += pcm_data
target = FIRST_CHUNK if first else NEXT_CHUNK
while len(buffer) >= target:
if not streaming.is_set():
break
chunk, buffer = buffer[:target], buffer[target:]
ws.send(json.dumps({
"type": "agent.speak",
"event_id": event_id,
"audio": base64.b64encode(chunk).decode()
}))
first = False
target = NEXT_CHUNK
if streaming.is_set() and buffer:
ws.send(json.dumps({
"type": "agent.speak",
"event_id": event_id,
"audio": base64.b64encode(buffer).decode()
}))
ws.send(json.dumps({
"type": "agent.speak_end",
"event_id": event_id
}))
def interrupt(ws):
streaming.clear() # Stop sending chunks
ws.send(json.dumps({"type": "agent.interrupt"}))Keep-alive
Sessions time out after 5 minutes of inactivity. Send a keep-alive every 2-3 minutes:
import time, threading
def keep_alive_loop(ws, interval=120):
while True:
time.sleep(interval)
ws.send(json.dumps({
"type": "session.keep_alive",
"event_id": f"keepalive-{uuid4()}"
}))Debug: Test tone
If your TTS audio doesn't work, try a known-good sine wave to isolate the problem:
import struct, math
sample_rate = 24000
pcm = b''.join(
struct.pack('<h', int(32767 * 0.5 * math.sin(2 * math.pi * 440 * i / sample_rate)))
for i in range(sample_rate) # 1 second
)
# If this works but TTS doesn't → your TTS format is wrong, resample to 24KHzCommon audio mistakes
| Mistake | Result | Fix |
|---|---|---|
| 16KHz sample rate | Garbled/fast | Resample to 24KHz |
| 44.1KHz sample rate | Garbled/slow | Resample to 24KHz |
| WAV with headers | Clicking at start | Strip headers, send raw PCM |
| MP3/OGG | Silence | Decode to PCM first |
| Not base64 encoded | WebSocket error | Base64 encode before sending |
| Chunks > 1MB | Dropped | Split into ~1s chunks |
ElevenLabs Agent Plugin (add-on)
Bridges ElevenLabs Conversational AI agents with LiveAvatar video.
Requirements
- ElevenLabs API key with permissions:
convai_read,user_read,voices_read - ElevenLabs Agent ID
- Agent audio output configured as PCM 24K
Setup
1. Store key:
curl -X POST https://api.liveavatar.com/v1/secrets \
-H "X-API-KEY: <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"secret_type": "ELEVENLABS_API_KEY", "secret_value": "<key>", "secret_name": "ElevenLabs Agent Key"}'2. Session token:
{
"mode": "LITE",
"avatar_id": "<avatar_id>",
"elevenlabs_agent_config": {
"secret_id": "<secret_id>",
"agent_id": "<elevenlabs_agent_id>"
}
}Important: With this plugin, a LiveKit room is auto-created (no ws_url). It uses FULL Mode's event system (LiveKit data channels), NOT LITE's WebSocket. Don't mix the event systems.
BYO WebRTC (add-on)
LiveKit
{
"mode": "LITE",
"avatar_id": "<avatar_id>",
"livekit_config": {
"url": "wss://your-livekit-server.com",
"token": "<your_agent_token>"
}
}Agora
{
"mode": "LITE",
"avatar_id": "<avatar_id>",
"agora_config": {
"app_id": "<your_agora_app_id>",
"channel": "<your_agora_channel>"
}
}Sandbox
{"mode": "LITE", "is_sandbox": true, "avatar_id": "dd73ea75-1218-4ef3-92ce-606d5f7fbc0a"}~1 min sessions, no credits.
Session Teardown
Stop a session when you're done to free resources and stop credit usage:
curl -X DELETE https://api.liveavatar.com/v1/sessions \
-H "Authorization: Bearer <session_token>"Close the WebSocket connection after the DELETE call returns. If the WebSocket drops unexpectedly, the session will auto-terminate after the 5-minute inactivity timeout.
Gotchas
1. Audio format is king. PCM 16-bit, 24KHz, base64. Wrong format = garbled with NO error. 2. Wait for `connected`. Events before it are silently dropped. 3. LITE uses WebSocket, not LiveKit data channels. Don't use avatar.* events — those are FULL Mode. 4. ElevenLabs plugin is a hybrid. Configured as LITE but uses FULL Mode's event system. 5. `agent.speak_end` is required. Without it, avatar won't transition states. 6. 5-minute timeout. Send session.keep_alive every 2-3 minutes. 7. Same `event_id` for all chunks in one utterance. Each agent.speak chunk within a single response must share the same event_id. Using different IDs per chunk will break playback. 8. Interrupt requires stopping your send loop. Sending agent.interrupt alone isn't enough — you must also stop sending remaining audio chunks, or they'll queue up and play after the interrupt. 9. Teardown stops credit usage. Always DELETE /v1/sessions when done. Orphaned sessions burn credits until the 5-minute timeout kills them.
Related skills
How it compares
Use for HeyGen LiveAvatar runtime wiring; choose generic WebRTC skills when no HeyGen SDK is involved.
FAQ
What integration paths does LiveAvatar Integrate compare?
LiveAvatar Integrate evaluates Embed, FULL, and LITE HeyGen LiveAvatar modes against the developer's tech stack, then recommends the optimal path and guides step-by-step implementation.
Who should use LiveAvatar Integrate?
LiveAvatar Integrate suits developers shipping conversational or streaming avatar experiences who need structured HeyGen integration guidance rather than piecing together API docs manually.