Skip to content

Quickstart

This walks through the minimum needed to see your avatar talk: a tiny backend that mints a session, and a plain HTML page that streams the video and lets you type something for the avatar to say.

  • A 2wai secret key (sk_...), provisioned for your account
  • An avatar_id (e.g. avt_9f2c1a4b7e3d) and, optionally, a voice_id, both provided when your account was set up
  • Node.js 18+ or Python 3.10+ for the backend snippet below

Your backend calls POST /v1/sessions with your secret key and gets back a session_id (for your server to use) plus a client_token and websocket_url (safe to hand to the browser).

server.js
import express from 'express';
const app = express();
app.use(express.json());
// sk_... read from an environment variable, never hard-coded or shipped
// to a client.
const TWAI_SECRET_KEY = process.env.TWAI_SECRET_KEY;
app.post('/session', async (req, res) => {
const response = await fetch('https://api.2wai.ai/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${TWAI_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
avatar_id: 'avt_9f2c1a4b7e3d',
mode: 'video',
voice_id: 'voice_2b7e1a9c',
}),
});
if (!response.ok) {
const body = await response.json();
return res.status(response.status).json(body);
}
const { session_id, client_token, websocket_url } = await response.json();
// Only ever send these two fields to the browser. TWAI_SECRET_KEY stays here.
res.json({ session_id, client_token, websocket_url });
});
app.listen(3000, () => console.log('listening on :3000'));

Your server also needs a way to tell the avatar what to say. Add this alongside the route above:

server.js (continued)
app.post('/say', async (req, res) => {
const { session_id, text } = req.body;
const response = await fetch(`https://api.2wai.ai/v1/sessions/${session_id}/speak`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TWAI_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
});
res.status(response.status).json(await response.json());
});

This page asks your /session route for a token, connects the WebSocket, and appends the incoming video to a <video> element via MediaSource. Typing in the box and hitting send calls your /say route, which calls /v1/sessions/{id}/speak for you.

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>2wai avatar quickstart</title>
</head>
<body>
<video id="avatar" autoplay playsinline muted style="max-width: 480px;"></video>
<form id="chat-form">
<input id="chat-input" type="text" placeholder="Say something..." autocomplete="off" />
<button type="submit">Send</button>
</form>
<script type="module">
const video = document.getElementById('avatar');
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
// Must match exactly: used both to check browser support and to
// open the SourceBuffer.
const MIME_CODEC = 'video/mp4; codecs="avc1.42C01F, mp4a.40.2"';
let sessionId;
let sourceBuffer;
const queue = [];
function appendNext() {
if (queue.length === 0 || sourceBuffer.updating) return;
sourceBuffer.appendBuffer(queue.shift());
}
async function start() {
// Ask YOUR backend for a session. It holds the sk_ key; this page never does.
const res = await fetch('/session', { method: 'POST' });
const { session_id, client_token, websocket_url } = await res.json();
sessionId = session_id;
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
sourceBuffer = mediaSource.addSourceBuffer(MIME_CODEC);
sourceBuffer.mode = 'sequence';
sourceBuffer.addEventListener('updateend', appendNext);
// websocket_url already carries the client_token, so connect
// directly. No extra headers needed (and browsers can't set
// them on a WS handshake anyway).
const ws = new WebSocket(websocket_url);
ws.binaryType = 'arraybuffer';
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ type: 'session.ready' }));
});
ws.addEventListener('message', (event) => {
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
if (message.type === 'speaking') {
console.log('avatar speaking:', message.value); // "start" | "stop"
}
return;
}
// Binary frames are fMP4 segments: queue and append in order.
queue.push(event.data);
appendNext();
});
});
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = '';
// Your backend proxies this to POST /v1/sessions/{id}/speak with the sk_ key.
await fetch('/say', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, text }),
});
});
start();
</script>
</body>
</html>

Serve index.html from the same backend (or any static host that can reach /session and /say on your server), open it, and the avatar should appear and start rendering. Type something and hit send: the avatar will speak it back.

  1. Run your server (node server.js or uvicorn server:app --reload).
  2. Open index.html in a browser.
  3. Type a message and send it.