Quickstart
This walks through the minimum needed to see your avatar talk on Transport 0 (LiveKit) — a tiny backend that mints a session, and a plain HTML page that joins the LiveKit room, renders the avatar’s video track, and lets you type something for it to say.
Prerequisites
Section titled “Prerequisites”- A 2wai secret key (
sk_...), issued to your account - An
avatar_id(e.g.avt_...) and optionally avoice_id - Node.js 18+ or Python 3.10+ for the backend
- A browser with LiveKit’s web SDK (
livekit-clientfrom npm, or the CDN build used below)
Step 1: Mint a LiveKit session on your server
Section titled “Step 1: Mint a LiveKit session on your server”Your backend calls POST /v1/sessions with your secret key. The gateway returns livekit_url, participant_token, and room_name — all safe to hand to the client.
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_...', mode: 'video', voice_id: 'vc_...', // optional transport: 'livekit', // Transport 0 — the only live transport }), });
if (!response.ok) { const body = await response.json(); return res.status(response.status).json(body); }
// Response shape on Transport 0: // { session_id, transport: "livekit", // livekit_url, participant_token, room_name, agent_identity } const session = await response.json();
// Hand only the fields the client needs to join the room. TWAI_SECRET_KEY stays here. res.json({ session_id: session.session_id, livekit_url: session.livekit_url, participant_token: session.participant_token, room_name: session.room_name, });});
app.listen(3000, () => console.log('listening on :3000'));import osfrom fastapi import FastAPIimport httpx
app = FastAPI()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_...", "mode": "video", "voice_id": "vc_...", # optional "transport": "livekit", # Transport 0 }, ) r.raise_for_status() s = r.json()
return { "session_id": s["session_id"], "livekit_url": s["livekit_url"], "participant_token": s["participant_token"], "room_name": s["room_name"], }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: Join the LiveKit room from the browser
Section titled “Step 2: Join the LiveKit room from the browser”This page asks your /session route for a token, joins the LiveKit room using the official livekit-client library, subscribes to the agent’s video + audio tracks, and renders them. Typing in the box calls your /say route.
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <title>2wai avatar quickstart</title> <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script> </head> <body> <video id="avatar" autoplay playsinline 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 { Room, RoomEvent } = LivekitClient;
const video = document.getElementById('avatar'); const form = document.getElementById('chat-form'); const input = document.getElementById('chat-input');
let sessionId;
async function start() { // Ask YOUR backend to mint a session. It holds the sk_; this page never does. const res = await fetch('/session', { method: 'POST' }); const { session_id, livekit_url, participant_token, room_name } = await res.json(); sessionId = session_id;
// Transport 0: adaptiveStream + dynacast MUST be off for 2wai agents. const room = new Room({ adaptiveStream: false, dynacast: false });
// The agent will join the room; subscribe to its video + audio when its tracks appear. room.on(RoomEvent.TrackSubscribed, (track) => { if (track.kind === 'video') { track.attach(video); } else if (track.kind === 'audio') { track.attach(); // creates a hidden <audio> and plays } });
await room.connect(livekit_url, participant_token); console.log('connected to room', room_name); }
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), open it, and the avatar should appear within a couple seconds of the LiveKit room connecting. Type something, hit send, the avatar speaks.
- Run your server (
node server.jsoruvicorn server:app --reload). - Open
index.htmlin a browser. - Type a message, hit send.