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.
Prerequisites
Section titled “Prerequisites”- A 2wai secret key (
sk_...), provisioned for your account - An
avatar_id(e.g.avt_9f2c1a4b7e3d) and, optionally, avoice_id, both provided when your account was set up - Node.js 18+ or Python 3.10+ for the backend snippet below
Step 1: Mint a session on your server
Section titled “Step 1: Mint a session on your server”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).
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'));import osfrom fastapi import FastAPIimport httpx
app = FastAPI()
# sk_... read from an environment variable, never hard-coded or shipped# to a client.TWAI_SECRET_KEY = os.environ["TWAI_SECRET_KEY"]
@app.post("/session")async def create_session(): async with httpx.AsyncClient() as client: r = await client.post( "https://api.2wai.ai/v1/sessions", headers={"Authorization": f"Bearer {TWAI_SECRET_KEY}"}, json={ "avatar_id": "avt_9f2c1a4b7e3d", "mode": "video", "voice_id": "voice_2b7e1a9c", }, ) r.raise_for_status() data = r.json()
# Only ever return these two fields to the browser. TWAI_SECRET_KEY stays here. return { "session_id": data["session_id"], "client_token": data["client_token"], "websocket_url": data["websocket_url"], }Your server also needs a way to tell the avatar what to say. Add this alongside the route above:
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());});from fastapi import Request
@app.post("/say")async def say(request: Request): body = await request.json() async with httpx.AsyncClient() as client: r = await client.post( f"https://api.2wai.ai/v1/sessions/{body['session_id']}/speak", headers={"Authorization": f"Bearer {TWAI_SECRET_KEY}"}, json={"text": body["text"]}, ) return r.json()Step 2: Stream it into a page
Section titled “Step 2: Stream it into a page”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.
<!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.
- Run your server (
node server.jsoruvicorn server:app --reload). - Open
index.htmlin a browser. - Type a message and send it.
What’s next
Section titled “What’s next”- Streaming into your app: a fuller, production-ready version of the player above, with reconnect and backpressure handling.
- Bring your own brain: replace the hard-coded text with an LLM-generated reply.
- Authentication: the full two-tier auth model.