Skip to content

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.

  • A 2wai secret key (sk_...), issued to your account
  • An avatar_id (e.g. avt_...) and optionally a voice_id
  • Node.js 18+ or Python 3.10+ for the backend
  • A browser with LiveKit’s web SDK (livekit-client from 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.

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_...',
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'));

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());
});

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.

index.html
<!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.

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