LiveKitSession
The session lifecycle class. Manages the LiveKit room, transcript, audio/video tracks. Use this when you want full control over the UI but don't want to reimplement the connection layer.
ts
import { LiveKitSession } from "@livelayer/sdk";
const session = new LiveKitSession({
agentId: "agt_abc123",
baseUrl: "https://livelayer.app",
});
Constructor
ts
new LiveKitSession(options: SessionOptions)
ts
interface SessionOptions {
agentId: string;
baseUrl: string;
apiKey?: string;
sessionEndpoint?: string;
sessionBody?: Record<string, unknown>;
}
| Name | Type | Description |
|---|---|---|
| agentId* | string | Published agent ID. |
| baseUrl* | string | API base URL — typically https://livelayer.app. |
| apiKey | string | API key for cross-origin auth (sent as x-api-key header). |
| sessionEndpoint | string | Override the default /api/widget/session URL. Use when your server mints LiveKit tokens. |
| sessionBody | Record<string, unknown> | Extra body fields included in the session POST. Use for passing your auth context (userId, plan, etc.). |
Lifecycle
ts
async connect(): Promise<void>
async connectWithToken(url: string, token: string): Promise<void>
disconnect(): void
canResume(): boolean
getRoom(): Room | null
| Name | Type | Description |
|---|---|---|
| connect | () => Promise<void> | Fetch a session token from sessionEndpoint, join the LiveKit room, subscribe to tracks. |
| connectWithToken | (url, token) => Promise<void> | Skip token fetch — pass a pre-fetched LiveKit URL and token. Use for preview surfaces or custom auth flows. |
| disconnect | () => void | Disconnect from the room and clear transcript. Safe to call when already disconnected. |
| canResume | () => boolean | True if the session can be resumed via connect() (within the 5-min RESUME_WINDOW). |
| getRoom | () => Room | null | Access the underlying livekit-client Room. Use for advanced track management. |
Callbacks
Set callbacks directly on the instance — they fire on state changes:
ts
session.onConnectionStateChange = (state) => {
console.log("connection:", state);
// "idle" | "connecting" | "connected" | "error" | "disconnected"
};
session.onAgentStateChange = (state) => {
console.log("agent:", state);
// "idle" | "listening" | "thinking" | "speaking"
};
session.onTranscript = (entries) => {
for (const entry of entries) {
if (entry.final) console.log(`[${entry.role}] ${entry.text}`);
}
};
session.onAgentConfig = (config) => {
// { name, avatarImageUrl, idleLoopUrl? }
document.querySelector("#agent-name").textContent = config.name;
};
session.onAudioTrack = (audioElement) => {
// Mount this somewhere in the DOM to play audio
document.body.appendChild(audioElement);
};
session.onVideoTrack = (videoElement) => {
// Mount this where you want the agent's avatar video
document.querySelector("#avatar-container").appendChild(videoElement);
};
session.onVideoTrackRemoved = () => {
// Track ended; clean up any video element references
};
session.onError = (message, code) => {
console.error("session error:", code, message);
};
session.onDataMessage = (msg) => {
// Raw data-channel messages (custom commands, agent events)
console.log("data:", msg);
};
session.onResumabilityChange = (canResume) => {
// Fires on connect, expiry, reset
};
Error codes
onError receives an optional code:
| Code | Meaning |
|---|---|
key_wrong_org | API key doesn't match the agent's organization |
key_expired | API key revoked or expired |
agent_has_no_org | Agent isn't published yet |
MIC_PERMISSION_DENIED | Visitor denied mic access |
MIC_NOT_FOUND | No mic detected on the visitor's device |
AGENT_TIMEOUT | Agent didn't respond in time (server-side issue) |
ROOM_DISCONNECT | Network or server disconnected the room |
Minimal end-to-end example
ts
import { LiveKitSession } from "@livelayer/sdk";
const session = new LiveKitSession({
agentId: "agt_abc123",
baseUrl: "https://livelayer.app",
});
const transcript = document.querySelector("#transcript")!;
const avatar = document.querySelector("#avatar")!;
session.onTranscript = (entries) => {
transcript.innerHTML = entries
.filter((e) => e.final)
.map((e) => `<p><strong>${e.role}:</strong> ${e.text}</p>`)
.join("");
};
session.onAudioTrack = (audio) => document.body.appendChild(audio);
session.onVideoTrack = (video) => avatar.appendChild(video);
document.querySelector("#talk")!.addEventListener("click", () => {
session.connect();
});
document.querySelector("#end")!.addEventListener("click", () => {
session.disconnect();
});
Mic publishing (you handle it)
LiveKitSession does not publish the visitor's mic for you. You must do this yourself once connected:
ts
import { createLocalAudioTrack } from "livekit-client";
session.onConnectionStateChange = async (state) => {
if (state !== "connected") return;
const room = session.getRoom();
if (!room) return;
const micTrack = await createLocalAudioTrack();
await room.localParticipant.publishTrack(micTrack);
};
The React package wraps this for you via useMicrophoneState. When using the SDK directly, mic management is your responsibility.
Persistence
The SDK includes helpers to save and resume sessions across page loads (within a 5-minute window):
ts
import { saveSession, loadSession, clearSession } from "@livelayer/sdk";
// On disconnect — save state for resume
session.onConnectionStateChange = (state) => {
if (state === "connected") {
const room = session.getRoom();
if (room) saveSession({ token: "...", roomName: room.name, ts: Date.now() });
}
if (state === "disconnected") {
clearSession();
}
};
// On boot — try to resume
const saved = loadSession();
if (saved) {
// Within 30s grace period
await session.connectWithToken(saved.url, saved.token);
}
Read next
<livelayer-widget>— UI on top ofLiveKitSessionLiveLayerTracker— visitor tracking- TypeScript types — all session-related types