How can I generate a LiveKit token for web interac...
# ask-ai
e
How can I generate a LiveKit token for web interaction with an agent in Next.js when the "Start Call" button is clicked?
t
Thanks for reaching out, Aditya. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you want to generate a LiveKit token in a Next.js app when a user clicks “Start Call” so the web client can join a room with an agent. Below is a secure, minimal pattern you can use: a server-side API route that creates the token, and a frontend button that requests it and connects. Server-side (secure token generation) • Create environment variables in `.env.local`:
Copy code
LIVEKIT_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
):
Copy code
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.
Copy code
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 | LiveKit
e
How would i pass this token to the session provider ?
t
You're asking how to pass the LiveKit token you generate on the server to a session provider in your Next.js app so the app can manage the LiveKit connection across components. Below is a concise, practical pattern using a React context/provider (for example, a
LiveKitProvider
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.
Copy code
// 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.
Copy code
// 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
.
Copy code
// 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:
Copy code
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?
e
I use TokenSource.custom to fetch token from the api when the component mounts, instead i want to make sure only when start call button is clicked then only token is fetched not before.
Copy code
export 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 fixes
t
Thanks — I see you’re using
TokenSource.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:
Copy code
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:
Copy code
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 | DeepWiki
e
All the hooks and the components except ChatterboxContent is imported from livekit, so suggest me best of your knowledge
t
Thanks — since you’re using LiveKit’s hooks/components for everything else, the cleanest approach is to keep using LiveKit primitives but create the TokenSource and trigger the session start only when the user clicks “Start Call.” You should avoid creating the TokenSource at mount (useMemo) and avoid calling hooks inside callbacks. Below are two LiveKit-friendly patterns — pick the one that matches your setup. Option 1 — Preferred when you can call session.start() and set a token source on the Session instance • Create the Session (or call
useSession()
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):
Copy code
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):
Copy code
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.
e
ok fuck this, no body updated you on the latest knowledge, tell me how do i provide tokens to livekit provider ?
t
Thanks — I hear your frustration. Let’s cut to the simplest, correct pattern so the token is fetched only when the user clicks “Start Call” and then provided to LiveKit’s provider/room components. Goal: defer token fetch until button press and hand that token (or TokenSource) to LiveKit so the connection happens only when you want. Recommended (cleanest) approach — use TokenSource.custom and call .fetch() on button click, then give the result to the LiveKit provider/room: 1) Create a TokenSource.custom but don’t call it on mount • TokenSource.custom only runs the inner async when you call its .fetch() method. • Keep the TokenSource instance available, but only .fetch() during Start Click. Example (React + TypeScript, minimal):
Copy code
'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.