Build with Anima
live avatars.
Build real-time video avatars, voice chat, and text conversations with the API or your AI assistant.


From portrait to conversation
- Create your avatarAdd an image, voice, and personality.
- Start a sessionChoose video, voice, or text.
- Connect your appStream replies to your users.
Build with AI / MCP
Use Sequencer in ChatGPT or Claude for assistant-driven setup. For an app integration, give your coding assistant these docs as Markdown.
Use Sequencer to create a live avatar from my attached portrait. Choose a warm English voice, keep replies short, and prepare it for a conversation. Share the avatar link and quote the cost before starting a call.https://mcp.sequencer.media/MCP tool reference
| Tool | Use |
|---|---|
list_live_avatars | Find existing avatars before creating one. |
list_live_avatar_voices | Find a real preset voice and its voiceId. |
create_live_avatar | Save an avatar draft. Supply a catalog voice ID or custom voice reference. |
get_live_avatar | Read settings, active version, and preparation progress. |
update_live_avatar | Update supplied fields. Read the avatar first. |
prepare_live_avatar | Prepare the saved avatar for live video. |
get_live_avatar_readiness | Check preparation and runtime readiness. |
quote_live_avatar_session | Read the USD reservation for a bounded conversation. |
start_live_avatar_session | Start an approved, bounded voice and video conversation. |
send_live_avatar_message | Send text into the active voice conversation. |
stop_live_avatar_session | End the call and settle connected-time billing. |
link_live_avatar_to_worldstream | Connect an avatar to an existing Worldstream draft. |
API quickstart
Create an API key, then list your workspaces. Keep the key on your server and send it in the x-api-key header.
https://api.sequencer.media/v1// Node.js 20+, on your server
const response = await fetch('https://api.sequencer.media/v1/workspaces', {
headers: { 'x-api-key': process.env.SEQUENCER_API_KEY }
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Request failed');
console.log(data.workspaces); // Select a workspace's id# pip install requests
import os, requests
response = requests.get(
"https://api.sequencer.media/v1/workspaces",
headers={"x-api-key": os.environ["SEQUENCER_API_KEY"]},
timeout=30,
)
response.raise_for_status()
print(response.json()["workspaces"])export SEQUENCER_API_KEY="sk_your_api_key"
export SEQUENCER_API_BASE="https://api.sequencer.media/v1"
curl --fail-with-body "$SEQUENCER_API_BASE/workspaces" \
-H "x-api-key: $SEQUENCER_API_KEY"Set your environment variables
# Server environment, or your local shell for the cURL examples.
export SEQUENCER_API_KEY="YOUR_API_KEY"
export SEQUENCER_API_BASE="https://api.sequencer.media/v1"
export APP_ORIGIN="https://your-app.example"
# Set SEQUENCER_WORKSPACE_ID after listing workspaces.
# Set SEQUENCER_CHARACTER_ID after creating your avatar.Use workspaces[].id as SEQUENCER_WORKSPACE_ID. Examples use server environment variables. The browser receives session credentials from your authenticated backend.
Create an avatar
Choose a voice from the catalog, then send your portrait and personality. Save liveCharacter.id as SEQUENCER_CHARACTER_ID.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Maya\",
\"avatarImageUrl\": \"https://your-cdn.example/maya.jpg\",
\"voiceSource\": \"preset\",
\"providerVoiceId\": \"$SEQUENCER_VOICE_ID\",
\"systemPrompt\": \"You are Maya, a friendly, playful guide. Speak casually and keep replies to one or two sentences.\",
\"runtimeProvider\": \"sequencer\",
\"animaMode\": \"fast\",
\"language\": \"en\",
\"maxSessionMinutes\": 5
}"
# Save liveCharacter.id from the response.
export SEQUENCER_CHARACTER_ID="your_character_id"curl --fail-with-body -X PATCH \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"systemPrompt": "You are Maya. Be warm, witty, and concise. Ask one natural follow-up question at a time.",
"knowledgeText": "Support hours: Monday to Friday, 9am to 5pm Pacific.",
"openingLines": ["Hey, what brings you here?", "Hi! What are we getting into today?"],
"cerebro": {
"personalityTraits": { "extraversion": 75, "playfulness": 80 }
},
"maxSessionMinutes": 5
}'- avatarImageUrl
- Stable HTTPS portrait URL.
- systemPrompt
- Role, tone, and response instructions.
- knowledgeText
- Facts your avatar should know.
- openingLines
- Optional greetings to choose from.
- orientation
- square, vertical, or horizontal.
- animaMode
- fast or max (when enabled).
- allowedOrigins
- Web app origins permitted to start sessions.
- maxSessionSeconds
- Maximum call duration for this avatar.
Pass image and voice URLs when creating or updating. Direct portrait and voice uploads are available through the Sequencer interface.
Choose a voice
Use voices[].voiceId as providerVoiceId. Set voiceSource to preset or custom.
# Use an id from workspaces[].id, not a workspace name or URL slug.
export SEQUENCER_WORKSPACE_ID="your_workspace_id"
curl --fail-with-body "$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-character-voices/presets" \
-H "x-api-key: $SEQUENCER_API_KEY"
# Select voices[].voiceId from the returned catalog.
export SEQUENCER_VOICE_ID="voice_id_from_catalog"// POST /workspaces/{workspaceId}/live-characters
// Or PATCH /workspaces/{workspaceId}/live-characters/{characterId}
{
"voiceSource": "custom",
"voiceReferenceUrl": "https://your-cdn.example/maya-voice.wav",
"voiceDescription": "A warm, casual American English voice. Relaxed pace and playful delivery."
}Listen with GET /live-character-voices/presets/{voiceId}/preview. It returns audio bytes.
Prepare for video
Prepare the saved avatar, then check liveCharacter.status is active and runtimeStatus is ready. Voice-only and text chat can start without video preparation.
curl --fail-with-body --max-time 900 -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/prepare" \
-H "x-api-key: $SEQUENCER_API_KEY"
# Get the latest configuration and preparation status.
curl --fail-with-body \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID" \
-H "x-api-key: $SEQUENCER_API_KEY"While preparation is queued or running, poll the avatar. If it fails, inspect runtimeError before retrying.
Your backend
Start sessions from your server. Choose the channels your app needs, then return the session credentials to that user.
| Experience | text | voice | video | Connection |
|---|---|---|---|---|
| Video avatar | true | true | true | LiveKit audio and video |
| Voice only | true | true | false | WebSocket audio and transcripts |
| Text only | true | false | false | WebSocket transcripts |
// avatar-api.js, server only, Node.js 20+
const BASE = 'https://api.sequencer.media/v1';
const key = process.env.SEQUENCER_API_KEY;
const workspace = process.env.SEQUENCER_WORKSPACE_ID;
const character = process.env.SEQUENCER_CHARACTER_ID;
if (!key || !workspace || !character) throw new Error('Missing avatar configuration');
const wsPath = '/workspaces/' + encodeURIComponent(workspace);
export async function avatarApi(path, { method = 'GET', body, origin } = {}) {
const response = await fetch(BASE + wsPath + path, {
method,
headers: {
'x-api-key': key,
'Content-Type': 'application/json',
// For sessions, set this to your configured web app origin.
...(origin ? { Origin: origin } : {})
},
...(body ? { body: JSON.stringify(body) } : {})
});
const data = await response.json();
if (!response.ok) {
const error = new Error(data.error || 'Avatar API request failed');
error.status = response.status;
error.retryAfter = response.headers.get('Retry-After');
throw error;
}
return data;
}
export function startAvatar(userId, channels, conversationHistory = [], startToken) {
return avatarApi('/live-characters/' + encodeURIComponent(character) + '/sessions', {
method: 'POST',
origin: process.env.APP_ORIGIN, // e.g. https://your-app.example
body: {
launchMode: 'webrtc', sessionMode: 'conversation',
channels, endUserId: userId, maxSessionSeconds: 300,
saveHistory: false,
...(conversationHistory.length ? { conversationHistory } : {}),
...(startToken ? { startToken } : {})
}
});
}
export function endAvatar(sessionId) {
return avatarApi('/live-character-sessions/' + encodeURIComponent(sessionId) + '/end', {
method: 'POST', body: { reason: 'USER_CLOSED' }
});
}// Express example, mount these on your own backend.
import express from 'express';
import { startAvatar, endAvatar } from './avatar-api.js';
import { requireSignedInUser, sessionStore, historyStore } from './your-app-auth.js';
const router = express.Router();
router.use(express.json(), requireSignedInUser);
const modes = {
text: { text: true, voice: false, video: false },
voice: { text: true, voice: true, video: false },
video: { text: true, voice: true, video: true }
};
router.post('/session', async (req, res, next) => {
try {
const channels = modes[req.body.mode];
if (!channels) return res.status(400).json({ error: 'Unknown mode' });
// req.user.id comes from your authentication middleware.
// historyStore is your own database, scoped by authenticated user and avatar.
const history = await historyStore.recent({
userId: req.user.id, workspaceId: process.env.SEQUENCER_WORKSPACE_ID,
characterId: process.env.SEQUENCER_CHARACTER_ID, limit: 60, maxBytes: 65536
});
const session = await startAvatar(req.user.id, channels, history);
try {
await sessionStore.save(session.sessionId, { userId: req.user.id });
} catch (error) {
await endAvatar(session.sessionId);
throw error;
}
res.json(session); // Session credentials only, never your API key.
} catch (error) { next(error); }
});
router.post('/session/:id/end', async (req, res, next) => {
try {
const owned = await sessionStore.get(req.params.id);
if (owned?.userId !== req.user.id) return res.sendStatus(404);
res.json(await endAvatar(req.params.id));
} catch (error) { next(error); }
});
export default router; // app.use('/api/avatar', router)
// Add your app's error handler to return error.status and a safe message.Set SEQUENCER_API_KEY, SEQUENCER_WORKSPACE_ID, SEQUENCER_CHARACTER_ID, and APP_ORIGIN. Supply your app’s authentication and session/history stores. Store session ownership and authorize every end or message request.
Video avatar
Start a video conversation, then connect with livekitUrl and livekitClientToken. Attach the avatar’s audio/video tracks and publish the user’s microphone.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/sessions" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Origin: https://your-app.example" \
-H "Content-Type: application/json" \
-d '{
"launchMode": "webrtc",
"sessionMode": "conversation",
"channels": {
"text": true,
"voice": true,
"video": true
},
"endUserId": "visitor_123",
"saveHistory": false,
"maxSessionSeconds": 300
}'// HTML: <button id="start">Start</button><div id="avatar"></div>
// <button id="end">End call</button>
import { Room, RoomEvent, Track } from 'https://sequencer.media/realtime/client-v1.js';
let room, session, starting = false, ending = false;
const container = document.querySelector('#avatar');
async function endCall() {
if (ending) return;
ending = true;
try {
await room?.disconnect(); // Also unpublishes the microphone.
container.replaceChildren();
if (session) await fetch('/api/avatar/session/' + session.sessionId + '/end', { method: 'POST' });
room = session = undefined;
} finally { ending = false; }
}
document.querySelector('#start').onclick = async () => {
if (session || starting || ending) return;
starting = true;
room = new Room();
room.on(RoomEvent.TrackSubscribed, track => {
if (track.kind !== Track.Kind.Video && track.kind !== Track.Kind.Audio) return;
const element = track.attach();
if (track.kind === Track.Kind.Video) {
element.playsInline = true; element.style.width = '100%';
}
container.appendChild(element);
});
room.on(RoomEvent.TrackUnsubscribed, track => {
track.detach().forEach(element => element.remove());
});
room.on(RoomEvent.Disconnected, () => { void endCall().catch(console.error); });
try {
const response = await fetch('/api/avatar/session', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'video' })
});
if (!response.ok) throw new Error('Could not start video session');
session = await response.json();
await room.connect(session.livekitUrl, session.livekitClientToken);
await room.startAudio();
await room.localParticipant.setMicrophoneEnabled(true);
} catch (error) { await endCall(); throw error; }
finally { starting = false; }
};
document.querySelector('#end').onclick = endCall;
// Provide an Enable audio button that calls room.startAudio() if autoplay is blocked.
// For typed input, use your backend's messages route from Send a text turn.Browser examples use the backend routes above. Start from a user click. End the API session after disconnecting. For an explicit spending cap, use quote and set a cost cap.
Voice-only chat
Use the avatar’s personality and voice in an audio conversation. Set video: false and connect to conversationStream.
- Request microphone access from a user click, then start a voice session.
- Open the WebSocket, send the session token, and wait for
ready. - Send
microphone_start, wait formicrophone_ready, then stream mono PCM16 audio. Play returnedaudioevents and displaytranscriptevents.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/sessions" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Origin: https://your-app.example" \
-H "Content-Type: application/json" \
-d '{
"launchMode": "webrtc",
"sessionMode": "conversation",
"channels": {
"text": true,
"voice": true,
"video": false
},
"endUserId": "visitor_123",
"saveHistory": false,
"maxSessionSeconds": 300
}'// Start from a click. Reuse connectConversation from the Text chat guide.
import { connectConversation } from './conversation.js';
export async function startVoiceChat() {
const context = new AudioContext({ sampleRate: 48000 });
await context.resume();
const activeAudio = new Set();
let nextAudio = 0, microphoneReady = false, session, socket, mic, capture, closing = false;
const clearAudio = () => {
for (const source of activeAudio) source.stop();
activeAudio.clear(); nextAudio = 0;
};
const cleanup = async () => {
if (closing) return;
closing = true;
microphoneReady = false;
capture?.disconnect();
mic?.getTracks().forEach(track => track.stop());
socket?.close(); clearAudio();
await context.close();
if (session) await fetch('/api/avatar/session/' + session.sessionId + '/end', { method: 'POST' });
};
try {
// Request permission before allocating a paid session.
mic = await navigator.mediaDevices.getUserMedia({ audio: {
channelCount: 1, echoCancellation: true, noiseSuppression: true
} });
await context.audioWorklet.addModule('/pcm-capture.js');
const response = await fetch('/api/avatar/session', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'voice' })
});
if (!response.ok) throw new Error('Could not start voice session');
session = await response.json();
socket = await connectConversation(session, event => {
if (event.type === 'microphone_ready') microphoneReady = true;
if (event.type === 'transcript') console.log(event.role, event.text, event.final);
if (event.type === 'error') console.error(event.message);
if (event.type === 'interrupt') clearAudio();
if (event.type !== 'audio') return;
const bytes = Uint8Array.from(atob(event.pcm), char => char.charCodeAt(0));
const view = new DataView(bytes.buffer);
const buffer = context.createBuffer(1, bytes.length / 2, event.sampleRate);
const samples = buffer.getChannelData(0);
for (let i = 0; i < samples.length; i++) samples[i] = view.getInt16(i * 2, true) / 32768;
const source = context.createBufferSource();
source.buffer = buffer; source.connect(context.destination);
const when = Math.max(context.currentTime + 0.02, nextAudio);
nextAudio = when + buffer.duration;
activeAudio.add(source);
source.onended = () => activeAudio.delete(source);
source.start(when);
}, () => { void cleanup().catch(console.error); });
socket.send(JSON.stringify({ type: 'microphone_start', sampleRate: context.sampleRate }));
const input = context.createMediaStreamSource(mic);
capture = new AudioWorkletNode(context, 'pcm-capture');
capture.port.onmessage = ({ data }) => {
if (microphoneReady && socket.readyState === WebSocket.OPEN) {
if (socket.bufferedAmount > 192000) { socket.close(); return; }
socket.send(data); // Mono little-endian PCM16 binary, not encoded WebM.
}
};
const silent = context.createGain(); silent.gain.value = 0;
input.connect(capture).connect(silent).connect(context.destination);
return cleanup; // Call on your End button, including on errors.
} catch (error) { await cleanup(); throw error; }
}// public/pcm-capture.js, served from your app's origin
class PcmCapture extends AudioWorkletProcessor {
process(inputs) {
const channels = inputs[0];
if (!channels?.length) return true;
const count = channels[0].length;
const pcm = new ArrayBuffer(count * 2);
const view = new DataView(pcm);
for (let i = 0; i < count; i++) {
let mono = 0;
for (const channel of channels) mono += channel[i] / channels.length;
mono = Math.max(-1, Math.min(1, mono));
view.setInt16(i * 2, mono < 0 ? mono * 32768 : mono * 32767, true);
}
this.port.postMessage(pcm, [pcm]);
return true;
}
}
registerProcessor('pcm-capture', PcmCapture);// conversation.js, browser
export function connectConversation(session, onEvent, onClose = () => {}) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(session.conversationStream.url);
let ready = false;
const timer = setTimeout(() => {
reject(new Error('Conversation connection timed out'));
socket.close();
}, 15000);
socket.onopen = () => socket.send(JSON.stringify({
token: session.conversationStream.token, sampleRate: 48000
}));
socket.onmessage = ({ data }) => {
const event = JSON.parse(data);
if (event.type === 'ready') {
ready = true;
clearTimeout(timer);
resolve(socket);
}
if (event.type === 'error' && !ready) {
clearTimeout(timer);
reject(new Error(event.message));
socket.close();
}
onEvent(event);
};
socket.onerror = () => {
if (!ready) { clearTimeout(timer); reject(new Error('WebSocket connection failed')); }
};
socket.onclose = () => {
clearTimeout(timer);
if (!ready) reject(new Error('Connection closed before ready'));
onClose();
};
});
}Serve microphone access over HTTPS or localhost. Call the cleanup function returned by startVoiceChat() from your End button and on navigation. Outgoing audio is binary little-endian PCM16; incoming audio is base64 PCM16 at each event’s sampleRate. Clear queued playback on interrupt.
Text chat
Set voice: false and video: false. Use the same WebSocket helper to send text_turn and receive transcript events. Replace streamed messages by their id.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/sessions" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Origin: https://your-app.example" \
-H "Content-Type: application/json" \
-d '{
"launchMode": "webrtc",
"sessionMode": "conversation",
"channels": {
"text": true,
"voice": false,
"video": false
},
"endUserId": "visitor_123",
"saveHistory": false,
"maxSessionSeconds": 300
}'// HTML: <button id="start">Start</button>
// <form id="chat"><input id="message" required><button>Send</button></form>
// <div id="transcript"></div><button id="end">End chat</button>
import { connectConversation } from './conversation.js';
let session, socket, starting = false, ending;
const messages = new Map();
function upsert(message) {
messages.set(message.id, message);
const lines = [...messages.values()].map(m => m.role + ': ' + m.text);
document.querySelector('#transcript').textContent = lines.join('\n');
}
async function endChat() {
if (ending) return ending;
const current = session;
session = undefined;
socket?.close(); socket = undefined;
ending = current
? fetch('/api/avatar/session/' + current.sessionId + '/end', { method: 'POST' })
: Promise.resolve();
try { await ending; } finally { ending = undefined; }
}
document.querySelector('#start').onclick = async () => {
if (session || starting || ending) return;
starting = true;
try {
const response = await fetch('/api/avatar/session', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'text' })
});
if (!response.ok) throw new Error('Could not start session');
session = await response.json();
messages.clear();
socket = await connectConversation(session, event => {
if (event.type === 'ready') event.messages.forEach(upsert);
if (event.type === 'transcript') upsert(event); // Replace by id as text streams.
if (event.type === 'error') console.error(event.message);
}, () => { void endChat().catch(console.error); });
} catch (error) { await endChat(); throw error; }
finally { starting = false; }
};
document.querySelector('#chat').onsubmit = event => {
event.preventDefault();
const input = document.querySelector('#message');
const text = input.value.trim();
if (!text || socket?.readyState !== WebSocket.OPEN) return;
socket.send(JSON.stringify({ type: 'text_turn', id: crypto.randomUUID(), text }));
input.value = '';
};
document.querySelector('#end').onclick = endChat;// conversation.js, browser
export function connectConversation(session, onEvent, onClose = () => {}) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(session.conversationStream.url);
let ready = false;
const timer = setTimeout(() => {
reject(new Error('Conversation connection timed out'));
socket.close();
}, 15000);
socket.onopen = () => socket.send(JSON.stringify({
token: session.conversationStream.token, sampleRate: 48000
}));
socket.onmessage = ({ data }) => {
const event = JSON.parse(data);
if (event.type === 'ready') {
ready = true;
clearTimeout(timer);
resolve(socket);
}
if (event.type === 'error' && !ready) {
clearTimeout(timer);
reject(new Error(event.message));
socket.close();
}
onEvent(event);
};
socket.onerror = () => {
if (!ready) { clearTimeout(timer); reject(new Error('WebSocket connection failed')); }
};
socket.onclose = () => {
clearTimeout(timer);
if (!ready) reject(new Error('Connection closed before ready'));
onClose();
};
});
}Your own voice agent
Already have a voice agent? Set sessionMode: audio_stream and send its speech through LiveKit to produce synchronized avatar video.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/sessions" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Origin: https://your-app.example" \
-H "Content-Type: application/json" \
-d '{
"launchMode": "webrtc",
"sessionMode": "audio_stream",
"channels": {
"text": false,
"voice": true,
"video": true
},
"endUserId": "visitor_123",
"saveHistory": false,
"maxSessionSeconds": 300
}'// Browser: connect to LiveKit using the Connect with LiveKit guide.
// Feed your own agent's speech into a Web Audio destination.
const context = new AudioContext();
await context.resume();
const output = context.createMediaStreamDestination();
// Connect your speech-producing AudioNode to output.
// Do not also connect that speech to speakers, or it will play twice.
const speechTrack = output.stream.getAudioTracks()[0];
await room.localParticipant.publishTrack(speechTrack, {
source: Track.Source.Microphone, dtx: false
});
// Keep this track alive through pauses. Let returned audio finish before ending.
// On End: disconnect the room, close the AudioContext, and call End Session.Prepare the avatar for video first. Publish your agent’s speech as the microphone track. Keep it alive through pauses, then disconnect and end the session.
Manage sessions
Keep the returned sessionId to send messages, read metrics, and end the call. Bounded starts also return avatarSessionId.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-character-sessions/{sessionId}/messages" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Hey Maya, what should we try first?" }'curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-character-sessions/{sessionId}/end" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "USER_CLOSED" }'
# For an abandoned start that has not returned a sessionId:
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-character-session-starts/{startToken}/cancel" \
-H "x-api-key: $SEQUENCER_API_KEY"// Default: your backend owns and supplies history for the authenticated user.
{
"saveHistory": false,
"endUserId": "visitor_123",
"conversationHistory": [
{ "role": "user", "content": "My dog is named Luna." },
{ "role": "assistant", "content": "How is Luna doing?" }
]
}
// Add the usual channels and session fields. Save final transcript events in
// your backend and supply the relevant recent messages when starting again.
// Maximum: 60 messages, 16000 characters per message, 64 KiB total UTF-8 content.
// New API conversations use client-managed history. Your backend chooses
// its retention policy. Hosted history creation is reserved for Sequencer's
// own products. Existing legacy hosted conversation IDs can still resume.// Server only. Reuse avatar-api.js from Your backend.
import { avatarApi } from './avatar-api.js';
const characterPath = '/live-characters/' +
encodeURIComponent(process.env.SEQUENCER_CHARACTER_ID);
// Show reservedUsd to the user before starting a video call.
const quote = await avatarApi(characterPath + '/sessions/quote', {
method: 'POST', body: { maxSessionSeconds: 60 }
});
console.log(quote.reservedUsd);
// Invoke from an authenticated handler after the amount is approved.
export function startQuotedVideo(userId, approvedMaxCostUsd) {
return avatarApi(characterPath + '/sessions', {
method: 'POST', origin: process.env.APP_ORIGIN,
body: {
launchMode: 'webrtc', sessionMode: 'conversation',
channels: { text: true, voice: true, video: true },
endUserId: userId, saveHistory: false,
maxSessionSeconds: quote.maxSessionSeconds,
maxCostUsd: approvedMaxCostUsd
}
});
}
// Store returned sessionId against this user. End it when finished.
// /bounded-sessions is also available for prepared video avatars;
// it returns avatarSessionId alongside the connection fields.# Upload a source clip and describe when to use it.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/custom-animations/upload" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-F "file=@wave.mp4" -F "name=Wave" -F "trigger=Wave when greeting a visitor"
# Prepare the returned customAnimation.id.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-characters/$SEQUENCER_CHARACTER_ID/custom-animations/{animationId}/prepare" \
-H "x-api-key: $SEQUENCER_API_KEY"
# Once its status is ready, trigger it in an active video conversation.
curl --fail-with-body -X POST \
"$SEQUENCER_API_BASE/workspaces/$SEQUENCER_WORKSPACE_ID/live-character-sessions/{sessionId}/animations/trigger" \
-H "x-api-key: $SEQUENCER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "animationId": "your_animation_id" }'- Billing
- Session start reserves funds. End reconciles usage and returns unused reservation. Text, voice, and video have different rates.
- Video cost cap
- Quote
reservedUsd, then pass an approvedmaxCostUsdandmaxSessionSeconds(30–3,600) when starting video. The quote is for video conversations. - History
- Store transcripts in your backend. Supply up to 60 user/assistant messages in
conversationHistory, withsaveHistory: falseand your authenticatedendUserId. - Errors
- Check the HTTP status and JSON
error. Verify key and workspace access for 401/403. HonorRetry-Afteron 429. End sessions after connection failure.
All avatar API endpoints
Expand an endpoint for request and response details. Paths below are relative to /v1/workspaces/{workspaceId}, except /workspaces.
Workspaces & characters
GETList workspaces/workspaces
/v1/workspaces- Request
- No body. Requires an API key.
- Response
- workspaces[].id and workspace metadata. Use the id in subsequent paths.
GETList avatars/live-characters
/v1/workspaces/{workspaceId}/live-characters- Request
- No body.
- Response
- liveCharacters[] with saved configuration and preparation status.
POSTCreate an avatar/live-characters
/v1/workspaces/{workspaceId}/live-characters- Request
- JSON: name, avatarImageUrl, systemPrompt, voiceSource, providerVoiceId (preset) or voiceReferenceUrl and voiceDescription (custom). Set runtimeProvider to sequencer. Optional knowledgeText, openingLines, language, orientation, animaMode, allowedOrigins, maxSessionSeconds, cerebro and avatarAnimationSettings.
- Response
- 201. liveCharacter.id, configuration and status. Save the id as characterId.
GETGet an avatar/live-characters/{characterId}
/v1/workspaces/{workspaceId}/live-characters/{characterId}- Request
- No body.
- Response
- liveCharacter, including runtimeStatus, prepStatus, runtimeError, activeVersionId and customAnimations. For video, wait for status: active and runtimeStatus: ready.
PATCHUpdate an avatar/live-characters/{characterId}
/v1/workspaces/{workspaceId}/live-characters/{characterId}- Request
- JSON: only changed creation fields. status can be active, paused or archived. Image, voice and mode changes can require new video preparation.
- Response
- Updated liveCharacter.
POSTPrepare video assets/live-characters/{characterId}/prepare
/v1/workspaces/{workspaceId}/live-characters/{characterId}/prepare- Request
- No body required. Poll GET avatar for progress.
- Response
- liveCharacter, prepared and prepJobId. Queued work can outlive the request. Skip video preparation for text and voice-only sessions.
DELETEArchive an avatar/live-characters/{characterId}
/v1/workspaces/{workspaceId}/live-characters/{characterId}- Request
- No body. Requires write access.
- Response
- Archives the avatar. New sessions cannot start for an archived character.
POSTPreview a workspace move/live-characters/move/preview
/v1/workspaces/{workspaceId}/live-characters/move/preview- Request
- JSON: characterIds (array). Requires write access to the source workspace.
- Response
- Affected characters, worlds, counts and canMoveCharactersOnly. Review dependencies before moving.
POSTMove avatars/live-characters/move
/v1/workspaces/{workspaceId}/live-characters/move- Request
- JSON: characterIds, targetWorkspaceId, includeWorlds (boolean). Requires write access to both workspaces. Preview dependencies first.
- Response
- Moved characterIds, sourceWorkspaceId, targetWorkspaceId, movedCount, movedWorldCount and warnings.
Voices & preparation
GETList preset voices/live-character-voices/presets
/v1/workspaces/{workspaceId}/live-character-voices/presets- Request
- No body. Filter the returned catalog in your application.
- Response
- voices[].voiceId and voice metadata. Pass voiceId as providerVoiceId when saving an avatar.
GETPreview a voice/live-character-voices/presets/{voiceId}/preview
/v1/workspaces/{workspaceId}/live-character-voices/presets/{voiceId}/preview- Request
- voiceId from the catalog.
- Response
- Audio bytes with the appropriate Content-Type. This response is not JSON.
POSTProvision generated voice audio/live-character-voices/provision
/v1/workspaces/{workspaceId}/live-character-voices/provision- Request
- JSON: sourceMediaId, voiceReferenceUrl, voiceDescription (at least 20 characters), language (optional). sourceMediaId must identify completed audio in this workspace and its URL must match. For a normal custom voice, set voiceReferenceUrl on the avatar instead.
- Response
- 200 when ready, otherwise 202. Voice draft status and providerVoiceId when available.
POSTDescribe a portrait/live-character-visual-description
/v1/workspaces/{workspaceId}/live-character-visual-description- Request
- JSON: avatarImageUrl (or sourceImageUri). Requires write access.
- Response
- characterVisualDescription generated from the image.
POSTCheck or warm the runtime/live-character-runtime/prepare
/v1/workspaces/{workspaceId}/live-character-runtime/prepare- Request
- JSON: characterId, statusOnly, ttlSeconds and waitSeconds (optional). statusOnly: true inspects the cache. Warming can incur costs and requires write access.
- Response
- 200 if ready, otherwise 202. ready, status and cache or setup estimates. This is separate from preparing the saved avatar.
Sessions & playback
POSTStart a session/live-characters/{characterId}/sessions
/v1/workspaces/{workspaceId}/live-characters/{characterId}/sessions- Request
- JSON: launchMode: webrtc, sessionMode: conversation or audio_stream, channels: {text, voice, video}, endUserId and maxSessionSeconds. Optional startToken, initialText, dynamicVariables, conversationHistory and saveHistory: false. Forward your configured web app Origin header. For a cost cap, maxCostUsd is supported on video conversation sessions only.
- Response
- 201. sessionId, channels, billing and connection credentials. Video returns livekitUrl and livekitClientToken. Text and voice-only return conversationStream.url and conversationStream.token.
POSTQuote a bounded video call/live-characters/{characterId}/sessions/quote
/v1/workspaces/{workspaceId}/live-characters/{characterId}/sessions/quote- Request
- JSON: maxSessionSeconds (integer, 30 to 3600, within the avatar limit). This quote is for video conversation sessions.
- Response
- characterId, maxSessionSeconds, currency: usd, reservedUsd, ready and preparationRequired.
POSTStart a bounded video call/live-characters/{characterId}/bounded-sessions
/v1/workspaces/{workspaceId}/live-characters/{characterId}/bounded-sessions- Request
- JSON: maxSessionSeconds and maxCostUsd covering the quote. Optional endUserId and legacy conversationId. Requires a prepared avatar. Uses text, voice and video channels.
- Response
- 201. Video session connection fields plus avatarSessionId and maxCostUsd. Use avatarSessionId for message, metrics and end paths.
POSTCancel a pending start/live-character-session-starts/{startToken}/cancel
/v1/workspaces/{workspaceId}/live-character-session-starts/{startToken}/cancel- Request
- Use the startToken you supplied when starting. No body required.
- Response
- Cancellation status. If a start completes with a sessionId, end that session too.
POSTSend a text turn/live-character-sessions/{sessionId}/messages
/v1/workspaces/{workspaceId}/live-character-sessions/{sessionId}/messages- Request
- JSON: message. Up to 4,000 characters. Requires text channels and conversation mode. Receive replies through WebSocket transcripts or LiveKit playback.
- Response
- Video returns an acknowledgement. Lightweight sessions can return message and messageId. Use WebSocket transcript events for streamed replies.
GETRead session metrics/live-character-sessions/{sessionId}/metrics
/v1/workspaces/{workspaceId}/live-character-sessions/{sessionId}/metrics- Request
- Optional query: live=true for current runtime diagnostics.
- Response
- Session usage, billing and available runtime diagnostics.
POSTEnd a session/live-character-sessions/{sessionId}/end
/v1/workspaces/{workspaceId}/live-character-sessions/{sessionId}/end- Request
- JSON: reason (optional, defaults to USER_CLOSED). Close browser audio/video connections too.
- Response
- Ends the session and reconciles billing. Unused reservation is returned.
POSTStart a preset avatar/live-character-presets/{presetId}/sessions
/v1/workspaces/{workspaceId}/live-character-presets/{presetId}/sessions- Request
- A supported presetId and the same body as Start a session. Forward the configured app Origin.
- Response
- 201. A new session with the selected preset and the requested channels.
GETCheck a preset avatar/live-character-presets/{presetId}/status
/v1/workspaces/{workspaceId}/live-character-presets/{presetId}/status- Request
- A supported presetId. No body.
- Response
- Preset availability and preparation status.
GETRead the legacy embed page/live-character-sessions/{sessionId}/embed
/v1/workspaces/{workspaceId}/live-character-sessions/{sessionId}/embed- Request
- Query: token, the session embed capability. This is an HTML status page. Use LiveKit to implement working live video playback.
- Response
- HTML. A token is required. Video is only attached when a direct playback URL is enabled; this is not a ready-made LiveKit player.
Conversation history
GETRead legacy hosted history/avatar-conversations/{conversationId}
/v1/workspaces/{workspaceId}/avatar-conversations/{conversationId}- Request
- Optional query: endUserId, limit and cursor. Scope access to the original user. New API integrations use their own history store and supply conversationHistory at session start.
- Response
- Authorized hosted conversation and paginated messages. Existing hosted IDs can resume; new API sessions use client-managed history.
DELETEDelete legacy hosted history/avatar-conversations/{conversationId}
/v1/workspaces/{workspaceId}/avatar-conversations/{conversationId}- Request
- Optional query: endUserId. Requires authorized access to this conversation.
- Response
- 202. Schedules history deletion.
GETRead Worldstream history/worldstream-avatar-conversations/{conversationId}
/v1/workspaces/{workspaceId}/worldstream-avatar-conversations/{conversationId}- Request
- Signed-in viewer access to their own Worldstream conversation. Optional query: limit and cursor.
- Response
- Viewer-scoped hosted conversation history.
DELETEDelete Worldstream history/worldstream-avatar-conversations/{conversationId}
/v1/workspaces/{workspaceId}/worldstream-avatar-conversations/{conversationId}- Request
- Signed-in viewer access to their own Worldstream conversation.
- Response
- 202. Schedules viewer-scoped history deletion.
Animations & Worldstreams
POSTUpload an animation/live-characters/{characterId}/custom-animations/upload
/v1/workspaces/{workspaceId}/live-characters/{characterId}/custom-animations/upload- Request
- multipart/form-data: file (video clip), name and trigger. Optional animationId to replace a clip.
- Response
- 202. customAnimation.id and preparation status.
PATCHUpdate an animation/live-characters/{characterId}/custom-animations/{animationId}
/v1/workspaces/{workspaceId}/live-characters/{characterId}/custom-animations/{animationId}- Request
- JSON: name and/or trigger guidance.
- Response
- Updated custom animation metadata.
POSTPrepare an animation/live-characters/{characterId}/custom-animations/{animationId}/prepare
/v1/workspaces/{workspaceId}/live-characters/{characterId}/custom-animations/{animationId}/prepare- Request
- Optional JSON: force (boolean). Read the avatar for animation progress.
- Response
- 200 when prepared, otherwise 202.
DELETEDelete an animation/live-characters/{characterId}/custom-animations/{animationId}
/v1/workspaces/{workspaceId}/live-characters/{characterId}/custom-animations/{animationId}- Request
- No body.
- Response
- Removes the animation from the character.
POSTPlay an animation/live-character-sessions/{sessionId}/animations/trigger
/v1/workspaces/{workspaceId}/live-character-sessions/{sessionId}/animations/trigger- Request
- JSON: animationId of a prepared motion. Requires a compatible active video conversation.
- Response
- Animation trigger acknowledgement.
POSTLink an avatar to a Worldstream/live-worlds/{liveWorldId}/avatar-links
/v1/workspaces/{workspaceId}/live-worlds/{liveWorldId}/avatar-links- Request
- JSON: characterId and expectedVersionId from the current Worldstream draft.
- Response
- Updated draft with the avatar link. A stale expectedVersionId is rejected.
