enough-match-43171
03/15/2026, 6:25 PMtall-airline-16756
03/15/2026, 6:25 PMtall-airline-16756
03/15/2026, 6:26 PMLIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
LIVEKIT_URL=<wss://your-project.livekit.cloud>
• Example API route for Next.js Pages Router (pages/api/token.ts):
import type { NextApiRequest, NextApiResponse } from 'next';
import { AccessToken } from 'livekit-server-sdk';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { identity, roomName } = req.body;
if (!identity || !roomName) return res.status(400).json({ error: 'Missing identity or roomName' });
const at = new AccessToken(process.env.LIVEKIT_API_KEY!, process.env.LIVEKIT_API_SECRET!);
at.identity = identity;
at.name = identity;
at.grants = {
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
};
try {
const token = await at.toJwt();
return res.status(200).json({ token, url: process.env.LIVEKIT_URL });
} catch (err) {
return res.status(500).json({ error: 'Failed to generate token' });
}
}
• If you use the App Router, place a similar handler under app/api/token/route.ts and export a POST handler.
Frontend (request token on Start Call click and connect)
• When the user clicks “Start Call”, call the API and then use the returned token and URL to connect with LiveKit’s web SDK.
import { Room } from 'livekit-client'; // or use connect from livekit-client
async function startCall() {
const resp = await fetch('/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: 'user123', roomName: 'agent-room' }),
});
const data = await resp.json();
if (!resp.ok) { console.error(data); return; }
const { token, url } = data;
// Example using livekit-client `connect`:
const room = await import('livekit-client').then(m => m.connect(url, token));
// Now attach to UI, publish tracks, etc.
}
// In JSX:
// <button onClick={startCall}>Start Call</button>
Notes, security, and options
• Keep the API secret only on the server — never expose it to the browser.
• Provide unique identities per user (e.g., from your auth system) to avoid collisions.
• Adjust grants as needed: set `canPublish`/`canSubscribe` based on role (agent vs. participant).
• You can optionally generate short-lived tokens by adding expiration claims if desired.
If you’d like, I can:
• Provide the App Router version of the API route.
• Show a full example that creates and publishes audio/video tracks in the browser after connecting.
• Suggest an identity strategy (UUID vs. username) for your use case.
Which follow-up would help most?
Sources: server-sdk-js | livekit/livekit_composite | DeepWiki | Stream music over WebRTC using React and WebAudio | LiveKitenough-match-43171
03/15/2026, 6:26 PMtall-airline-16756
03/15/2026, 6:26 PMLiveKitProvider or your own SessionProvider) plus the token fetch from your API when the user clicks "Start Call".
I'll show: 1) A simple SessionProvider that accepts a connect call (using livekit-client's connect). 2) How to fetch the token on button click and hand it to the provider. 3) Notes on role/identity handling and lifecycle.
1) Session provider (simplified)
• This provider stores the connected Room and exposes connect/disconnect helpers and the room object to children.
// components/LiveKitProvider.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';
import { Room, connect as lkConnect } from 'livekit-client';
type LiveKitContextType = {
room: Room | null;
connecting: boolean;
connectToRoom: (url: string, token: string) => Promise<void>;
disconnect: () => void;
};
const LiveKitContext = createContext<LiveKitContextType | undefined>(undefined);
export const LiveKitProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [room, setRoom] = useState<Room | null>(null);
const [connecting, setConnecting] = useState(false);
const connectToRoom = useCallback(async (url: string, token: string) => {
if (room) return;
setConnecting(true);
try {
const r = await lkConnect(url, token);
setRoom(r);
// optional: handle room events, participants, etc.
r.on('disconnected', () => setRoom(null));
} finally {
setConnecting(false);
}
}, [room]);
const disconnect = useCallback(() => {
if (!room) return;
room.disconnect();
setRoom(null);
}, [room]);
return (
<LiveKitContext.Provider value={{ room, connecting, connectToRoom, disconnect }}>
{children}
</LiveKitContext.Provider>
);
};
export const useLiveKit = () => {
const ctx = useContext(LiveKitContext);
if (!ctx) throw new Error('useLiveKit must be used inside LiveKitProvider');
return ctx;
};
2) Fetch token on "Start Call" and pass to provider
• Call your server token route, then call connectToRoom(url, token) from the provider.
// components/StartCallButton.tsx
import React from 'react';
import { useLiveKit } from './LiveKitProvider';
export const StartCallButton: React.FC<{ identity: string; roomName: string }> = ({ identity, roomName }) => {
const { connectToRoom, connecting } = useLiveKit();
const startCall = async () => {
try {
const resp = await fetch('/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity, roomName }),
});
if (!resp.ok) {
const err = await resp.json();
console.error('Token error', err);
return;
}
const { token, url } = await resp.json();
// Pass token + url to the provider's connect method
await connectToRoom(url, token);
// After this, room is available via useLiveKit().room
} catch (e) {
console.error('Failed to start call', e);
}
};
return <button onClick={startCall} disabled={connecting}>{connecting ? 'Connecting...' : 'Start Call'}</button>;
};
3) Use provider at app root and consume room elsewhere
• Wrap your app with the provider so any component can access the room or call disconnect.
// pages/_app.tsx (Pages Router)
import { LiveKitProvider } from '../components/LiveKitProvider';
function MyApp({ Component, pageProps }) {
return (
<LiveKitProvider>
<Component {...pageProps} />
</LiveKitProvider>
);
}
export default MyApp;
Then in other components you can do:
const { room, disconnect } = useLiveKit();
// render participant list, local tracks, etc. using room
Notes and recommendations
• Identity: pass a stable unique identity (user id or UUID) when requesting the token so provider/connect can use a unique participant identity.
• Token lifetime: tokens are typically short-lived. Request a token right before connecting rather than issuing it long in advance.
• Error handling: show UI errors if token fetch fails or connect throws.
• Role-based grants: request tokens with different grants (publish/subscribe) for agent vs. visitor, and then call connect using that token.
• Reconnect: if you need to refresh tokens on long sessions, call your token API again and reconnect or use LiveKit server-side refresh logic.
If you'd like, I can:
• Provide an App Router example for app/layout wrapping.
• Show how to auto-publish local audio/video after connect.
• Add error UI + reconnect logic for token expiry.
Which follow-up would be most helpful?enough-match-43171
03/15/2026, 6:29 PMexport function Chatterbox({
baseUrl = "",
publishableKey,
identifier,
tools,
}: ChatterboxProps) {
const draggableRef = useRef<HTMLDivElement>(null)
const [error, setError] = useState<string | null>(null)
const serializedTools = useMemo(
() => (tools && tools.length > 0 ? serializeTools(tools) : []),
[tools]
)
const tokenSource = useMemo(
() =>
TokenSource.custom(async () => {
const response = await fetch(`${baseUrl}/api/auth/livekit`, {
method: "POST",
body: JSON.stringify({
publishableKey,
identifier,
tools: serializedTools,
}),
})
if (!response.ok) {
console.log('response status', response.status)
const body = await response.json().catch(() => ({}))
setError(
body.error ?? `Token request failed (${response.status})`
)
return null
}
return response.json()
}),
[baseUrl, publishableKey, identifier, serializedTools]
)
const session = useSession(tokenSource)
useDraggable(draggableRef as RefObject<HTMLElement>)
return (
<AgentSessionProvider session={session}>
<ChatterboxContent
error={error}
setError={setError}
baseUrl={baseUrl}
publishableKey={publishableKey}
draggableRef={draggableRef}
tools={tools}
/>
</AgentSessionProvider>
)
}
Suggest me fixestall-airline-16756
03/15/2026, 6:30 PMTokenSource.custom inside a useMemo which causes the token fetch to run when the component mounts. You want the token to be requested only when the user clicks “Start Call.” Below are two clean approaches you can use depending on whether you want to keep using the Session APIs or manage the connection manually.
I’ll briefly explain each and provide code you can drop into your component.
Option A — Keep Session APIs but create TokenSource lazily and call session.start()
• Create the session at mount without a token source.
• Create the TokenSource only when the user clicks Start, set it on the session, then call session.start() to trigger token fetch + connect.
• This keeps hooks at top-level and avoids calling hooks inside callbacks.
Example:
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { TokenSource, Session, useSession } from '@livekit/components-core';
export function Chatterbox(props: ChatterboxProps) {
const { baseUrl = '', publishableKey, identifier, tools } = props;
const serializedTools = useMemo(() => (tools?.length ? serializeTools(tools) : []), [tools]);
// create session hook without a token source so it does not auto-fetch
// pass `null` or a placeholder token source depending on your session API; here we create Session manually
const [session] = useState(() => new Session());
// if you use a useSession hook from your library, call it at top-level and control start() later:
// const session = useSession(null);
const [error, setError] = useState<string | null>(null);
const draggableRef = useRef<HTMLDivElement>(null);
const startCall = useCallback(async () => {
// build TokenSource only when button clicked
const tokenSource = TokenSource.custom(async () => {
const response = await fetch(`${baseUrl}/api/auth/livekit`, {
method: 'POST',
body: JSON.stringify({
publishableKey,
identifier,
tools: serializedTools,
}),
});
if (!response.ok) {
const body = await response.json().catch(() => ({}));
setError(body.error ?? `Token request failed (${response.status})`);
return null;
}
return response.json();
});
try {
// attach token source to the session and start connection
session.setTokenSource?.(tokenSource); // if Session has a setTokenSource method
await session.start?.(); // triggers token fetch + connect
} catch (e) {
setError((e as Error).message ?? 'Failed to start session');
}
}, [baseUrl, publishableKey, identifier, serializedTools, session]);
useDraggable(draggableRef);
return (
<>
<button onClick={startCall}>Start Call</button>
<AgentSessionProvider session={session}>
<ChatterboxContent
error={error}
setError={setError}
baseUrl={baseUrl}
publishableKey={publishableKey}
draggableRef={draggableRef}
tools={tools}
/>
</AgentSessionProvider>
</>
);
}
Notes for Option A:
• Check your Session API for methods like setTokenSource, start, or connect. If the Session implementation exposes a way to set a TokenSource and then trigger start, use those. This avoids calling hooks inside callbacks and defers token fetch until start().
Option B — Manually fetch token on button click and connect session
• Fetch the token imperatively in the click handler, then use the session/client connect method directly. This is straightforward and avoids relying on TokenSource hooking behavior.
Example:
import { useState, useMemo, useRef, useCallback } from 'react';
import { connect as lkConnect, Room } from 'livekit-client';
export function Chatterbox(props: ChatterboxProps) {
const { baseUrl = '', publishableKey, identifier, tools } = props;
const serializedTools = useMemo(() => (tools?.length ? serializeTools(tools) : []), [tools]);
const [room, setRoom] = useState<Room | null>(null);
const [error, setError] = useState<string | null>(null);
const draggableRef = useRef<HTMLDivElement>(null);
const startCall = useCallback(async () => {
try {
const resp = await fetch(`${baseUrl}/api/auth/livekit`, {
method: 'POST',
body: JSON.stringify({ publishableKey, identifier, tools: serializedTools }),
});
if (!resp.ok) {
const body = await resp.json().catch(() => ({}));
setError(body.error ?? `Token request failed (${resp.status})`);
return;
}
const { participantToken, serverUrl } = await resp.json();
// directly connect using livekit-client
const r = await lkConnect(serverUrl, participantToken);
setRoom(r);
} catch (e) {
setError((e as Error).message ?? 'Failed to start call');
}
}, [baseUrl, publishableKey, identifier, serializedTools]);
useDraggable(draggableRef);
return (
<>
<button onClick={startCall} disabled={!!room}>Start Call</button>
{room ? (
<AgentSessionProvider session={room /* or wrap room in your session shape */}>
<ChatterboxContent
error={error}
setError={setError}
baseUrl={baseUrl}
publishableKey={publishableKey}
draggableRef={draggableRef}
tools={tools}
/>
</AgentSessionProvider>
) : null}
</>
);
}
Notes for Option B:
• The API response must include the fields you need (e.g., participantToken and serverUrl). Adjust to your API shape.
• This approach gives you explicit control over when the token is fetched and when the connection occurs.
Which option fits your stack?
• If you want to keep using the higher-level Session APIs and methods like session.start, use Option A and attach a TokenSource lazily, then call start.
• If you prefer explicit control and simplicity, Option B (manual fetch + lkConnect) is robust and straightforward.
If you tell me which Session API you’re using (the exact library and versions or whether `useSession`/`Session` exposes setTokenSource / start), I’ll give the exact code tailored to that API (App Router vs Pages Router, or @livekit/components-core versions).
Sources: Build agent experiences faster with Session APIs | LiveKit | server-sdk-js | livekit/livekit_composite | DeepWiki | client-sdk-js | livekit/livekit_composite | DeepWikienough-match-43171
03/15/2026, 6:32 PMtall-airline-16756
03/15/2026, 6:32 PMuseSession() without a TokenSource) at top-level so hooks remain valid.
• Create the TokenSource.custom lazily inside the Start button handler.
• Attach the TokenSource to the Session (via session.setTokenSource or similar) and call session.start() to trigger token fetch and connect.
Example (uses LiveKit Session API methods and keeps hooks at top-level):
import { useMemo, useRef, useState, useCallback } from 'react';
import { TokenSource, Session, useSession, AgentSessionProvider } from '@livekit/components'; // adjust exact imports
export function Chatterbox({ baseUrl = '', publishableKey, identifier, tools }: ChatterboxProps) {
const serializedTools = useMemo(() => (tools?.length ? serializeTools(tools) : []), [tools]);
const draggableRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
// create session hook at top-level but DO NOT pass a token source yet
// if your library exposes `useSession(null)` do that, otherwise create Session instance and use it with AgentSessionProvider
const session = useSession(null); // or: const [session] = useState(() => new Session());
const startCall = useCallback(async () => {
// create TokenSource only when Start is clicked
const tokenSource = TokenSource.custom(async () => {
const res = await fetch(`${baseUrl}/api/auth/livekit`, {
method: 'POST',
body: JSON.stringify({ publishableKey, identifier, tools: serializedTools }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError(body.error ?? `Token request failed (${res.status})`);
return null;
}
return res.json();
});
try {
// attach token source and start the session (these methods may vary by version)
if (typeof session.setTokenSource === 'function') {
session.setTokenSource(tokenSource);
}
// call start to trigger token fetch + connect
await session.start();
} catch (e) {
setError((e as Error).message ?? 'Failed to start session');
}
}, [baseUrl, publishableKey, identifier, serializedTools, session]);
useDraggable(draggableRef);
return (
<>
<button onClick={startCall}>Start Call</button>
<AgentSessionProvider session={session}>
<ChatterboxContent
error={error}
setError={setError}
baseUrl={baseUrl}
publishableKey={publishableKey}
draggableRef={draggableRef}
tools={tools}
/>
</AgentSessionProvider>
</>
);
}
Notes for Option 1:
• Confirm your Session exposes setTokenSource (or equivalent) and start() methods. That API exists in LiveKit’s UI session wrappers; if the exact method names differ, replace accordingly.
• This defers token fetching until start() is called and preserves the session lifecycle via LiveKit’s components and provider.
Option 2 — If Session doesn’t expose setTokenSource / start(), fetch token imperatively and call Session connect directly
• Fetch token on click, then call the LiveKit connect API (or session.connect) with the returned serverUrl + token.
• This is explicit and works with any LiveKit client or wrapper.
Example (imperative token fetch + connect):
import { useMemo, useRef, useState, useCallback } from 'react';
import { connect as lkConnect } from 'livekit-client';
import { AgentSessionProvider } from '@livekit/components';
export function Chatterbox({ baseUrl = '', publishableKey, identifier, tools }: ChatterboxProps) {
const serializedTools = useMemo(() => (tools?.length ? serializeTools(tools) : []), [tools]);
const draggableRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
const [sessionObj, setSessionObj] = useState<any | null>(null);
const startCall = useCallback(async () => {
try {
const res = await fetch(`${baseUrl}/api/auth/livekit`, {
method: 'POST',
body: JSON.stringify({ publishableKey, identifier, tools: serializedTools }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setError(body.error ?? `Token request failed (${res.status})`);
return;
}
const { participantToken, serverUrl } = await res.json();
// connect using livekit-client directly
const room = await lkConnect(serverUrl, participantToken);
setSessionObj(room); // pass this into AgentSessionProvider if it accepts a Room
} catch (e) {
setError((e as Error).message ?? 'Failed to start call');
}
}, [baseUrl, publishableKey, identifier, serializedTools]);
useDraggable(draggableRef);
return (
<>
<button onClick={startCall} disabled={!!sessionObj}>Start Call</button>
<AgentSessionProvider session={sessionObj}>
<ChatterboxContent
error={error}
setError={setError}
baseUrl={baseUrl}
publishableKey={publishableKey}
draggableRef={draggableRef}
tools={tools}
/>
</AgentSessionProvider>
</>
);
}
Notes for Option 2:
• Ensure your token API returns the fields you need (commonly participantToken and serverUrl). Adjust names to match your response.
• This offers precise control and avoids depending on session helper methods.
Which approach is best for you?
• Use Option 1 if your LiveKit Session API supports attaching a TokenSource and calling start(): it keeps you fully inside LiveKit’s hook/component model and is tidy.
• Use Option 2 if your Session wrapper does not offer `setTokenSource`/`start()` or if you prefer explicit control over the token fetch and connection.
If you tell me the exact LiveKit package names and versions you import (for example the exact package that provides useSession, TokenSource, and AgentSessionProvider), I’ll provide the exact implementation that matches those method names and types.enough-match-43171
03/15/2026, 6:35 PMtall-airline-16756
03/15/2026, 6:35 PM'use client';
import { useMemo, useState, useRef, useCallback } from 'react';
import { TokenSource } from 'livekit-client';
import { LiveKitRoom } from '@livekit/components-react'; // or the component you use
// adjust imports to your LiveKit package names
export function Chatterbox({ baseUrl, publishableKey, identifier, tools }: ChatterboxProps) {
const [connected, setConnected] = useState(false);
const [tokenError, setTokenError] = useState<string | null>(null);
const tokenSource = useMemo(() => {
return TokenSource.custom(async () => {
const res = await fetch(`${baseUrl}/api/auth/livekit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ publishableKey, identifier, tools }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? `Token request failed (${res.status})`);
}
// return shape: { serverUrl: string, participantToken: string } or what your client expects
return res.json();
});
}, [baseUrl, publishableKey, identifier, tools]);
const handleStart = useCallback(async () => {
setTokenError(null);
try {
// fetch token only on click
const result = await tokenSource.fetch();
if (!result) throw new Error('No token returned');
// Option A: If your LiveKit UI component accepts a tokenSource prop, just set a flag to render it:
setConnected(true);
// Option B: If your provider requires a token string/serverUrl directly, keep the result in state and pass it in instead:
// setTokenResult(result);
} catch (e) {
setTokenError((e as Error).message);
}
}, [tokenSource]);
return (
<>
{!connected && <button onClick={handleStart}>Start Call</button>}
{tokenError && <div className="error">{tokenError}</div>}
{connected && (
// Many LiveKit UI components accept a TokenSource directly.
// If your LiveKitRoom / AgentSessionProvider accepts `tokenSource`, pass it.
<LiveKitRoom tokenSource={tokenSource} connect={true} audio={true} video={true}>
<ChatterboxContent /* props */ />
</LiveKitRoom>
)}
</>
);
}
2) Two common provider/usage patterns — choose the one your library expects
• Pattern A — Provider accepts TokenSource:
◦ Pass the TokenSource instance and set connect={true} or call the provider’s start() method.
◦ The provider will call tokenSource.fetch() internally when it needs to connect (or you call .fetch() yourself before rendering).
• Pattern B — Provider expects serverUrl + token:
◦ Call const { serverUrl, participantToken } = await tokenSource.fetch() inside the Start handler, store those in state, then render the provider with those values:
▪︎ <AgentSessionProvider serverUrl={serverUrl} token={participantToken}>...</AgentSessionProvider>
3) Key implementation details and tips
• Do not create the TokenSource by calling the fetch function at mount — TokenSource.custom is safe to create at mount so long as you don’t call .fetch() until desired.
• If your LiveKit provider will automatically call tokenSource.fetch() when it mounts, only render the provider after the user clicks Start (as shown above).
• Handle errors from the fetch by catching and surfacing messages (setTokenError).
• Tokens are short-lived. Fetch them right before connecting rather than ahead of time.
• If you need repeated reconnects, call tokenSource.fetch() again when you need a fresh token.
If you tell me which exact LiveKit component you pass the token to (for example LiveKitRoom, AgentSessionProvider, or useSession + Session), and whether it expects a TokenSource or raw token/serverUrl, I’ll give the exact code to wire it up for that API.