Skip to main content

Endpoint token generation

Implement a LiveKit standardized token endpoint.

Overview

Build your own token endpoint for production use. Your backend generates JWT tokens, and your frontend uses an endpoint TokenSource to fetch them. By following the standard endpoint format below, the same endpoint works with all LiveKit client SDKs. You must add your own custom header-based authentication to the endpoint to ensure that your endpoint is secure.

This is the production alternative to the sandbox token server.

Endpoint schema

Request format:

TypeNameValue/Description
MethodPOST
HeadersContent-Typeapplication/json
Optional Bodyroom_name(Optional room name)
participant_identity(Optional participant identity)
participant_name(Optional participant name)
participant_metadata(Optional participant metadata)
participant_attributes(Optional participant attributes)
room_config(Optional room config)

Your endpoint should be able to accept any of these optional body fields and generate a token encoding the relevant token attributes. If a given field shouldn't be configurable, return a corresponding 4xx status code from the endpoint.

Agent dispatch

When room_config is provided with agent dispatch information, you should pass it directly to the access token builder. The client SDKs automatically package agent information (like agent_name and agent_metadata) into room_config before sending the request, so your endpoint implementation only needs to pass room_config to the token builder. This is essential for 1:1 agent applications. See the examples below for how to implement this.

Response format:

TypeNameValue/Description
Status Code201 (Created)
HeadersContent-Typeapplication/json
Response Bodyserver_urlRoom connection URL
participant_tokenRoom connection token

Use an endpoint-based TokenSource

This guide walks you through setting up a server to generate room connection credentials.

Sending authentication headers

If your token endpoint is protected (for example, with JWT or API key validation), the client must send credentials when requesting a token. Pass custom request options when creating the TokenSource so that each token request includes the required headers.

In JavaScript and TypeScript, TokenSource.endpoint accepts an optional second argument (RequestInit). Use the headers property to add an Authorization header or other auth headers. Swift, Kotlin, and Flutter also support custom headers on their endpoint TokenSource (see the examples in step 2 below).

  1. Install the LiveKit Server SDK:

    // go.mod
    module example_server
    go 1.21
    require (
    github.com/livekit/protocol v1.11.0
    )
    go mod init example_server
    go get github.com/livekit/protocol
    # yarn:
    yarn add livekit-server-sdk
    # npm:
    npm install livekit-server-sdk --save
    # Add to your Gemfile
    gem 'livekit-server-sdk'
    uv add livekit-api
    # Cargo.toml
    [package]
    name = "example_server"
    version = "0.1.0"
    edition = "2021"
    [dependencies]
    livekit-api = "0.2.0"
    # Remaining deps are for the example server
    warp = "0.3"
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    tokio = { version = "1", features = ["full"] }
    composer require agence104/livekit-server-sdk
  2. Create a new file named development.env with your connection URL, API key and secret:

    export LIVEKIT_URL=<your LiveKit server URL>
    export LIVEKIT_API_KEY=<your API Key>
    export LIVEKIT_API_SECRET=<your API Secret>
  3. Create a server to host an endpoint at /getToken, following the token endpoint specification:

    // server.go
    package main
    import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "github.com/livekit/protocol/auth"
    "github.com/livekit/protocol/livekit"
    )
    type TokenSourceRequest struct {
    RoomName string `json:"room_name"`
    ParticipantName string `json:"participant_name"`
    ParticipantIdentity string `json:"participant_identity"`
    ParticipantMetadata string `json:"participant_metadata"`
    ParticipantAttributes map[string]string `json:"participant_attributes"`
    RoomConfig *livekit.RoomConfiguration `json:"room_config"`
    }
    type TokenSourceResponse struct {
    ServerURL string `json:"server_url"`
    ParticipantToken string `json:"participant_token"`
    }
    func getJoinToken(body TokenSourceRequest) string {
    at := auth.NewAccessToken(os.Getenv("LIVEKIT_API_KEY"), os.Getenv("LIVEKIT_API_SECRET"))
    // If this room doesn't exist, it'll be automatically created when
    // the first participant joins
    roomName := body.RoomName
    if roomName == "" {
    roomName = "quickstart-room"
    }
    grant := &auth.VideoGrant{
    RoomJoin: true,
    Room: roomName,
    }
    at.AddGrant(grant)
    if body.RoomConfig != nil {
    at.SetRoomConfig(body.RoomConfig)
    }
    // Participant related fields.
    // `participantIdentity` will be available as LocalParticipant.identity
    // within the livekit-client SDK
    if body.ParticipantIdentity != "" {
    at.SetIdentity(body.ParticipantIdentity)
    } else {
    at.SetIdentity("quickstart-identity")
    }
    if body.ParticipantName != "" {
    at.SetName(body.ParticipantName)
    } else {
    at.SetName("quickstart-username")
    }
    if len(body.ParticipantMetadata) > 0 {
    at.SetMetadata(body.ParticipantMetadata)
    }
    if len(body.ParticipantAttributes) > 0 {
    at.SetAttributes(body.ParticipantAttributes)
    }
    token, _ := at.ToJWT()
    return token
    }
    func main() {
    http.HandleFunc("/getToken", func(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    // Declare a new Person struct to hold the decoded data
    var body TokenSourceRequest
    // Create a JSON decoder and decode the request body into the struct
    err := json.NewDecoder(r.Body).Decode(&body)
    if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
    }
    b, _ := json.Marshal(TokenSourceResponse{
    ServerURL: os.Getenv("LIVEKIT_URL"),
    ParticipantToken: getJoinToken(body),
    })
    w.Write(b)
    })
    log.Fatal(http.ListenAndServe(":3000", nil))
    }
    // server.js
    import express from 'express';
    import { AccessToken } from 'livekit-server-sdk';
    import { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';
    const app = express();
    const port = 3000;
    app.use(express.json());
    app.post('/getToken', async (req, res) => {
    const body = req.body;
    // If this room doesn't exist, it'll be automatically created when
    // the first participant joins
    const roomName = body.room_name ?? 'quickstart-room';
    // Participant related fields.
    // `participantIdentity` will be available as LocalParticipant.identity
    // within the livekit-client SDK
    const participantIdentity = body.participant_identity ?? 'quickstart-identity';
    const participantName = body.participant_name ?? 'quickstart-username';
    const participantMetadata = body.participant_metadata ?? '';
    const participantAttributes = body.participant_attributes ?? {};
    const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, {
    identity: participantIdentity,
    name: participantName,
    metadata: participantMetadata,
    attributes: participantAttributes,
    // Token to expire after 10 minutes
    ttl: '10m',
    });
    at.addGrant({ roomJoin: true, room: roomName });
    if (body.room_config) {
    at.roomConfig = new RoomConfiguration(body.room_config);
    }
    const participantToken = await at.toJwt();
    res.send({ server_url: process.env.LIVEKIT_URL, participant_token: participantToken });
    });
    app.listen(port, () => {
    console.log(`Server listening on port ${port}`);
    });
    # server.rb
    require 'livekit'
    require 'sinatra'
    require 'json'
    set :port, 3000
    def create_token(body)
    token = LiveKit::AccessToken.new(api_key: ENV['LIVEKIT_API_KEY'], api_secret: ENV['LIVEKIT_API_SECRET'])
    # If this room doesn't exist, it'll be automatically created when
    # the first participant joins
    room_name = body["room_name"] || 'quickstart-room';
    token.add_grant(roomJoin: true, room: room_name)
    token.room_config = body["room_config"] || {};
    # Participant related fields.
    # `participantIdentity` will be available as LocalParticipant.identity
    # within the livekit-client SDK
    token.identity = body["participant_identity"] || "quickstart-identity"
    token.name = body["participant_name"] || "quickstart-username"
    if body["participant_metadata"]
    token.metadata = body["participant_metadata"]
    end
    if body["participant_attributes"]
    token.attributes = body["participant_attributes"]
    end
    token.to_jwt
    end
    post '/getToken' do
    request.body.rewind # (in case it was already read)
    body_text = request.body.read
    body = body_text.empty? ? {} : JSON.parse(body_text)
    content_type :json
    JSON.generate({ "server_url" => ENV['LIVEKIT_URL'], "participant_token" => create_token(body) })
    end
    # server.py
    import os
    from livekit import api
    from flask import Flask, jsonify, request
    app = Flask(__name__)
    @app.route('/getToken', methods=['POST'])
    def getToken():
    body = request.get_json()
    token = api.AccessToken(os.getenv('LIVEKIT_API_KEY'), os.getenv('LIVEKIT_API_SECRET'))
    # If this room doesn't exist, it'll be automatically created when
    # the first participant joins
    room_name = body.get('room_name') or 'quickstart-room'
    token = token.with_grants(api.VideoGrants(room_join=True, room=room_name))
    if body.get('room_config'):
    token = token.with_room_config(body['room_config'])
    # Participant related fields.
    # `participantIdentity` will be available as LocalParticipant.identity
    # within the livekit-client SDK
    token = token.with_identity(body.get('participant_identity') or 'quickstart-identity')
    token = token.with_name(body.get('participant_name') or 'quickstart-username')
    if body.get('participant_metadata'):
    token = token.with_metadata(body['participant_metadata'])
    if body.get('participant_attributes'):
    token = token.with_attributes(body['participant_attributes'])
    return jsonify({
    'server_url': os.getenv('LIVEKIT_URL'),
    'participant_token': token.to_jwt()
    })
    if __name__ == '__main__':
    app.run(port=3000)
    // src/main.rs
    use livekit_api::access_token;
    use warp::Filter;
    use serde::{Serialize, Deserialize};
    use std::env;
    use std::collections::HashMap;
    #[derive(Deserialize)]
    struct TokenSourceRequest {
    #[serde(default)]
    room_name: Option<String>,
    #[serde(default)]
    participant_name: Option<String>,
    #[serde(default)]
    participant_identity: Option<String>,
    #[serde(default)]
    participant_metadata: Option<String>,
    #[serde(default)]
    participant_attributes: HashMap<String, String>,
    #[serde(default)]
    room_config: Option<livekit_protocol::RoomConfiguration>,
    }
    #[derive(Serialize)]
    struct TokenSourceResponse {
    server_url: String,
    participant_token: String,
    }
    #[tokio::main]
    async fn main() {
    let server_url = env::var("LIVEKIT_URL").expect("LIVEKIT_URL is not set");
    // Define the route
    let create_token_route = warp::path("getToken")
    .and(warp::body::json())
    .map(|body: TokenSourceRequest| {
    let participant_token = create_token(body).unwrap();
    warp::reply::json(&TokenSourceResponse { server_url, participant_token })
    });
    // Start the server
    warp::serve(create_token_route).run(([127, 0, 0, 1], 3000)).await;
    }
    // Token creation function
    fn create_token(body: TokenSourceRequest) -> Result<String, access_token::AccessTokenError> {
    let api_key = env::var("LIVEKIT_API_KEY").expect("LIVEKIT_API_KEY is not set");
    let api_secret = env::var("LIVEKIT_API_SECRET").expect("LIVEKIT_API_SECRET is not set");
    let mut token = access_token::AccessToken::with_api_key(&api_key, &api_secret);
    // If this room doesn't exist, it'll be automatically created when
    // the first participant joins
    let room_name = body.room_name.unwrap_or_else(|| "quickstart-room".to_string());
    token = token.with_grants(access_token::VideoGrants {
    room_join: true,
    room: room_name,
    ..Default::default()
    });
    if let Some(room_config) = body.room_config {
    token = token.with_room_config(room_config);
    }
    // Participant related fields.
    // `participantIdentity` will be available as LocalParticipant.identity
    // within the livekit-client SDK
    token = token
    .with_identity(body.participant_identity.unwrap_or_else(|| "quickstart-identity".to_string()))
    .with_name(body.participant_name.unwrap_or_else(|| "quickstart-username".to_string()));
    if let Some(participant_metadata) = body.participant_metadata {
    token = token.with_metadata(participant_metadata);
    }
    if !body.participant_attributes.is_empty() {
    token = token.with_attributes(body.participant_attributes);
    }
    token.to_jwt()
    }
    // Note: This example assumes the server is accessible on port 3000 to match the client SDK examples.
    // If using Apache/Nginx (typically ports 80/8080), configure your web server to proxy requests
    // to port 3000, or update the client SDK examples to use your server's port.
    // Get the incoming JSON request body
    $rawBody = file_get_contents('php://input');
    $body = json_decode($rawBody, true);
    // Validate that we have valid JSON
    if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode(['error' => 'Invalid JSON in request body']);
    exit;
    }
    // Define the token options.
    $tokenOptions = (new AccessTokenOptions())
    // Participant related fields.
    // `participantIdentity` will be available as LocalParticipant.identity
    // within the livekit-client SDK
    ->setIdentity($body['participant_identity'] ?? 'quickstart-identity')
    ->setName($body['participant_name'] ?? 'quickstart-username');
    if (!empty($body["participant_metadata"])) {
    $tokenOptions = $tokenOptions->setMetadata($body["participant_metadata"]);
    }
    if (!empty($body["participant_attributes"])) {
    $tokenOptions = $tokenOptions->setAttributes($body["participant_attributes"]);
    }
    // Define the video grants.
    $roomName = $body['room_name'] ?? 'quickstart-room';
    $videoGrant = (new VideoGrant())
    ->setRoomJoin()
    // If this room doesn't exist, it'll be automatically created when
    // the first participant joins
    ->setRoomName($roomName);
    $token = (new AccessToken(getenv('LIVEKIT_API_KEY'), getenv('LIVEKIT_API_SECRET')))
    ->init($tokenOptions)
    ->setGrant($videoGrant);
    if (!empty($body["room_config"])) {
    $token = $token->setRoomConfig($body["room_config"]);
    }
    echo json_encode([
    'server_url' => getenv('LIVEKIT_URL'),
    'participant_token' => $token->toJwt()
    ]);
  4. Load the environment variables and run the server:

    $ source development.env
    $ go run server.go
    $ source development.env
    $ node server.js
    $ source development.env
    $ ruby server.rb
    $ source development.env
    $ python server.py
    $ source development.env
    $ cargo r src/main.rs
    $ source development.env
    $ php server.php
    Note

    See the Tokens & grants page for more information on how to generate tokens with custom permissions.

  5. Consume your endpoint with a TokenSource:

    import { Room, TokenSource } from 'livekit-client';
    // Create the TokenSource. Pass a second argument to send custom headers (e.g. for endpoint auth).
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken", {
    // TODO: Add your authentication here
    // headers: {
    // Authorization: `Bearer ${getUserAuthToken()}`,
    // },
    });
    // Generate a new token
    const { serverUrl, participantToken } = await tokenSource.fetch({ roomName: "room name to join" });
    // Use the generated token to connect to a room
    const room = new Room();
    room.connect(serverUrl, participantToken);
    import { TokenSource } from 'livekit-client';
    import { useSession, SessionProvider } from '@livekit/components-react';
    // Create the TokenSource with auth headers so your protected endpoint can validate the request.
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken", {
    // TODO: Add your authentication here
    // headers: {
    // Authorization: `Bearer ${getUserAuthToken()}`,
    // },
    });
    export const MyPage = () => {
    const session = useSession(tokenSource, { roomName: "room name to join" });
    // Start the session when the component mounts, and end the session when the component unmounts
    useEffect(() => {
    session.start();
    return () => {
    session.end();
    };
    }, []);
    return (
    <SessionProvider session={session}>
    <MyComponent />
    </SessionProvider>
    )
    }
    export const MyComponent = () => {
    // Access the session available via the context to build your app
    // ie, show a list of all camera tracks:
    const cameraTracks = useTracks([Track.Source.Camera], {onlySubscribed: true});
    return (
    <>
    {cameraTracks.map((trackReference) => {
    return (
    <VideoTrack {...trackReference} />
    )
    })}
    </>
    )
    }
    import LiveKitComponents
    struct MyEndpointTokenSource: EndpointTokenSource {
    let url: URL
    let headers: [String: String]
    }
    @main
    struct SessionApp: App {
    let session: Session
    init() {
    let tokenSource = MyEndpointTokenSource(
    url: URL(string: "http://localhost:3000/getToken")!,
    headers: ["Authorization": "Bearer \(getUserAuthToken())"] // your app's auth token
    ).cached()
    session = Session(tokenSource: tokenSource, tokenOptions: TokenRequestOptions())
    }
    var body: some Scene {
    WindowGroup {
    ContentView()
    .environmentObject(session)
    .alert(session.error?.localizedDescription ?? "Error", isPresented: .constant(session.error != nil)) {
    Button(action: session.dismissError) { Text("OK") }
    }
    .alert(session.agent.error?.localizedDescription ?? "Error", isPresented: .constant(session.agent.error != nil)) {
    AsyncButton(action: session.end) { Text("OK") }
    }
    }
    }
    }
    struct ContentView: View {
    @EnvironmentObject var session: Session
    var body: some View {
    if session.isConnected {
    AsyncButton(action: session.end) {
    Text("Disconnect")
    }
    Text(String(describing: session.agent.agentState))
    } else {
    AsyncButton(action: session.start) {
    Text("Connect")
    }
    }
    }
    }
    val tokenRequestOptions = remember { TokenRequestOptions(roomName = "customRoom") }
    val tokenSource = remember {
    TokenSource.fromEndpoint(
    url = "http://localhost:3000/getToken",
    headers = mapOf("Authorization" to "Bearer ${getUserAuthToken()}"),
    ).cached()
    }
    val session = rememberSession(
    tokenSource = tokenSource,
    options = SessionOptions(
    tokenRequestOptions = tokenRequestOptions
    )
    )
    Column {
    SessionScope(session = session) { session ->
    val coroutineScope = rememberCoroutineScope()
    var shouldConnect by remember { mutableStateOf(false) }
    LaunchedEffect(shouldConnect) {
    if (shouldConnect) {
    val result = session.start()
    // Handle if the session fails to connect.
    if (result.isFailure) {
    Toast.makeText(context, "Error connecting to the session.", Toast.LENGTH_SHORT).show()
    shouldConnect = false
    }
    } else {
    session.end()
    }
    }
    Button(onClick = { shouldConnect = !shouldConnect }) {
    Text(
    if (shouldConnect) {
    "Disconnect"
    } else {
    "Connect"
    }
    )
    }
    }
    }
    import 'package:livekit_client/livekit_client.dart' as sdk;
    import 'package:livekit_components/livekit_components.dart' as components;
    class MyApp extends StatefulWidget {
    State<MyApp> createState() => _MyAppState();
    }
    class _MyAppState extends State<MyApp> {
    final tokenSource = sdk.EndpointTokenSource(
    url: Uri.parse("http://localhost:3000/getToken"),
    headers: {'Authorization': 'Bearer ${getUserAuthToken()}'},
    );
    late final session = sdk.Session.fromConfigurableTokenSource(tokenSource);
    void dispose() {
    session.dispose();
    super.dispose();
    }
    Widget build(BuildContext context) {
    return components.SessionContext(
    session: session,
    child: ListenableBuilder(
    listenable: session,
    builder: (context, _) {
    if (session.error != null) {
    return AlertDialog(
    title: Text('Error'),
    content: Text(session.error!.message),
    actions: [
    TextButton(
    onPressed: session.dismissError,
    child: Text('OK'),
    ),
    ],
    );
    }
    if (session.isConnected) {
    return ElevatedButton(
    onPressed: () => session.end(),
    child: Text('Disconnect'),
    );
    } else {
    return ElevatedButton(
    onPressed: () => session.start(),
    child: Text('Connect'),
    );
    }
    },
    ),
    );
    }
    }
    import { TokenSource } from 'livekit-client';
    import { useSession, SessionProvider } from '@livekit/components-react';
    // Create the TokenSource with auth headers for your protected endpoint.
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken", {
    // TODO: Add your authentication here
    // headers: {
    // Authorization: `Bearer ${getUserAuthToken()}`,
    // },
    });
    export const MyPage = () => {
    const session = useSession(tokenSource, { roomName: "room name to join" });
    // Start the session when the component mounts, and end the session when the component unmounts
    useEffect(() => {
    session.start();
    return () => {
    session.end();
    };
    }, []);
    return (
    <SessionProvider session={session}>
    {/* render the rest of your application here */}
    </SessionProvider>
    )
    }

Production endpoint examples

These complete, production-ready token endpoint implementations follow the standard endpoint format and include agent dispatch support. Each example is copy-paste ready—you can bring it into your backend, add your authentication layer (for example, JWT validation, session checks), and drop it into your app.

Complete example using Next.js App Router with TypeScript:

// app/api/token/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { AccessToken } from 'livekit-server-sdk';
import { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// TODO: Add your authentication here
// const user = await authenticateRequest(request);
// if (!user) {
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
// }
const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;
const serverUrl = process.env.LIVEKIT_URL;
if (!apiKey || !apiSecret || !serverUrl) {
return NextResponse.json(
{ error: 'Server configuration error' },
{ status: 500 }
);
}
const roomName = body.room_name || `room-${Date.now()}`;
const participantIdentity = body.participant_identity || `user-${Date.now()}`;
const participantName = body.participant_name || 'User';
const roomConfig = body.room_config;
const at = new AccessToken(apiKey, apiSecret, {
identity: participantIdentity,
name: participantName,
metadata: body.participant_metadata || '',
attributes: body.participant_attributes || {},
ttl: '10m',
});
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
});
if (roomConfig) {
at.roomConfig = new RoomConfiguration(roomConfig);
}
const participantToken = await at.toJwt();
return NextResponse.json(
{
server_url: serverUrl,
participant_token: participantToken,
},
{ status: 201 }
);
} catch (error) {
console.error('Token generation error:', error);
return NextResponse.json(
{ error: 'Failed to generate token' },
{ status: 500 }
);
}
}

Complete example using Django with REST framework:

# views.py or api/views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import json
import os
import time
from livekit import api
from livekit.api import RoomAgentDispatch, RoomConfiguration
@csrf_exempt
@require_http_methods(["POST"])
def get_token(request):
try:
body = json.loads(request.body)
# TODO: Add your authentication here
# from django.contrib.auth.decorators import login_required
# @login_required
# user = request.user
# NOTE: The below is fine for a self contained example, but consider making these environment
# variables django settings instead: https://docs.djangoproject.com/en/6.0/topics/settings/
api_key = os.getenv('LIVEKIT_API_KEY')
api_secret = os.getenv('LIVEKIT_API_SECRET')
server_url = os.getenv('LIVEKIT_URL')
if not all([api_key, api_secret, server_url]):
return JsonResponse(
{'error': 'Server configuration error'},
status=500
)
room_name = body.get('room_name') or f'room-{int(time.time())}'
participant_identity = body.get('participant_identity') or f'user-{int(time.time())}'
participant_name = body.get('participant_name') or 'User'
room_config = body.get('room_config')
token = api.AccessToken(api_key, api_secret) \
.with_identity(participant_identity) \
.with_name(participant_name) \
.with_grants(api.VideoGrants(
room_join=True,
room=room_name,
can_publish=True,
can_subscribe=True,
))
if body.get('participant_metadata'):
token = token.with_metadata(body['participant_metadata'])
if body.get('participant_attributes'):
token = token.with_attributes(body['participant_attributes'])
if body.get('room_config'):
token = token.with_room_config(body['room_config'])
participant_token = token.to_jwt()
return JsonResponse(
{
'server_url': server_url,
'participant_token': participant_token,
},
status=201
)
except Exception as e:
print(f'Token generation error: {e}')
return JsonResponse(
{'error': 'Failed to generate token'},
status=500
)

Add to your urls.py:

from django.urls import path
from .views import get_token
urlpatterns = [
path('api/token', get_token, name='get_token'),
]

Complete example using FastAPI:

# main.py or api/token.py
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from typing import Optional, Dict
import os
import time
from livekit import api
from livekit.api import RoomAgentDispatch, RoomConfiguration
app = FastAPI()
class TokenRequest(BaseModel):
room_name: Optional[str] = None
participant_identity: Optional[str] = None
participant_name: Optional[str] = None
participant_metadata: Optional[str] = None
participant_attributes: Optional[Dict[str, str]] = None
room_config: Optional[dict] = None
@app.post("/api/token", status_code=201)
async def get_token(request: TokenRequest):
try:
# TODO: Add your authentication here
# from fastapi import Depends, Header
# async def verify_token(authorization: str = Header(...)):
# # Verify JWT or session token
# pass
# Then add: token_data: dict = Depends(verify_token)
api_key = os.getenv('LIVEKIT_API_KEY')
api_secret = os.getenv('LIVEKIT_API_SECRET')
server_url = os.getenv('LIVEKIT_URL')
if not all([api_key, api_secret, server_url]):
raise HTTPException(
status_code=500,
detail='Server configuration error'
)
room_name = request.room_name or f'room-{int(time.time())}'
participant_identity = request.participant_identity or f'user-{int(time.time())}'
participant_name = request.participant_name or 'User'
token = api.AccessToken(api_key, api_secret) \
.with_identity(participant_identity) \
.with_name(participant_name) \
.with_grants(api.VideoGrants(
room_join=True,
room=room_name,
can_publish=True,
can_subscribe=True,
))
if request.participant_metadata:
token = token.with_metadata(request.participant_metadata)
if request.participant_attributes:
token = token.with_attributes(request.participant_attributes)
if request.room_config:
token = token.with_room_config(request.room_config)
participant_token = token.to_jwt()
return {
'server_url': server_url,
'participant_token': participant_token,
}
except Exception as e:
print(f'Token generation error: {e}')
raise HTTPException(
status_code=500,
detail='Failed to generate token'
)

Complete example using Ruby on Rails:

# app/controllers/token_controller.rb
class TokenController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_request # TODO: Add your authentication
def create
begin
api_key = ENV['LIVEKIT_API_KEY']
api_secret = ENV['LIVEKIT_API_SECRET']
server_url = ENV['LIVEKIT_URL']
unless api_key && api_secret && server_url
return render json: { error: 'Server configuration error' }, status: 500
end
room_name = params[:room_name] || "room-#{Time.now.to_i}"
participant_identity = params[:participant_identity] || "user-#{Time.now.to_i}"
participant_name = params[:participant_name] || 'User'
token = LiveKit::AccessToken.new(
api_key: api_key,
api_secret: api_secret
)
token.identity = participant_identity
token.name = participant_name
token.metadata = params[:participant_metadata] if params[:participant_metadata]
token.attributes = params[:participant_attributes] if params[:participant_attributes]
token.video_grant = LiveKit::VideoGrant.from_hash(
roomJoin: true,
room: room_name,
canPublish: true,
canSubscribe: true
)
# If room_config is provided, pass it directly to the token builder.
# The client SDKs automatically package agent dispatch information into room_config.
token.room_config = params[:room_config] if params[:room_config]
participant_token = token.to_jwt
render json: {
server_url: server_url,
participant_token: participant_token
}, status: 201
rescue => e
Rails.logger.error "Token generation error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: { error: 'Failed to generate token' }, status: 500
end
end
private
def authenticate_request
# TODO: Add your authentication logic here
# For example:
# token = request.headers['Authorization']&.split(' ')&.last
# @current_user = User.find_by_token(token)
# unless @current_user
# render json: { error: 'Unauthorized' }, status: 401
# end
end
end

Add to your config/routes.rb:

Rails.application.routes.draw do
post '/api/token', to: 'token#create'
end

Complete example using Spring Boot:

// TokenController.java
package com.example.api;
import io.livekit.server.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class TokenController {
@Value("${livekit.api.key}")
private String apiKey;
@Value("${livekit.api.secret}")
private String apiSecret;
@Value("${livekit.url}")
private String serverUrl;
@PostMapping("/token")
public ResponseEntity<?> getToken(@RequestBody TokenRequest request) {
try {
// TODO: Add your authentication here
// @PreAuthorize("isAuthenticated()")
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String roomName = request.getRoomName() != null
? request.getRoomName()
: "room-" + Instant.now().getEpochSecond();
String participantIdentity = request.getParticipantIdentity() != null
? request.getParticipantIdentity()
: "user-" + Instant.now().getEpochSecond();
String participantName = request.getParticipantName() != null
? request.getParticipantName()
: "User";
AccessToken token = new AccessToken(apiKey, apiSecret);
token.setIdentity(participantIdentity);
token.setName(participantName);
if (request.getParticipantMetadata() != null) {
token.setMetadata(request.getParticipantMetadata());
}
if (request.getParticipantAttributes() != null) {
token.setAttributes(request.getParticipantAttributes());
}
VideoGrant videoGrant = new VideoGrant();
videoGrant.setRoomJoin(true);
videoGrant.setRoom(roomName);
videoGrant.setCanPublish(true);
videoGrant.setCanSubscribe(true);
token.addGrant(videoGrant);
// If room_config is provided, pass it directly to the token builder.
// The client SDKs automatically package agent dispatch information into room_config.
if (request.getRoomConfig() != null) {
token.setRoomConfig(request.getRoomConfig());
}
String participantToken = token.toJwt();
Map<String, String> response = new HashMap<>();
response.put("server_url", serverUrl);
response.put("participant_token", participantToken);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (Exception e) {
System.err.println("Token generation error: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Failed to generate token"));
}
}
// Request DTO
public static class TokenRequest {
private String roomName;
private String participantIdentity;
private String participantName;
private String participantMetadata;
private Map<String, String> participantAttributes;
private RoomConfiguration roomConfig;
// Getters and setters
public String getRoomName() { return roomName; }
public void setRoomName(String roomName) { this.roomName = roomName; }
public String getParticipantIdentity() { return participantIdentity; }
public void setParticipantIdentity(String participantIdentity) {
this.participantIdentity = participantIdentity;
}
public String getParticipantName() { return participantName; }
public void setParticipantName(String participantName) {
this.participantName = participantName;
}
public String getParticipantMetadata() { return participantMetadata; }
public void setParticipantMetadata(String participantMetadata) {
this.participantMetadata = participantMetadata;
}
public Map<String, String> getParticipantAttributes() { return participantAttributes; }
public void setParticipantAttributes(Map<String, String> participantAttributes) {
this.participantAttributes = participantAttributes;
}
public RoomConfiguration getRoomConfig() { return roomConfig; }
public void setRoomConfig(RoomConfiguration roomConfig) {
this.roomConfig = roomConfig;
}
}
}

Adding authentication

All examples include a TODO comment where you should add your authentication layer. Common approaches include:

  • JWT validation: Verify a JWT token from your authentication service
  • Session validation: Check for a valid user session
  • API key validation: Validate an API key in the request headers
  • OAuth verification: Verify OAuth tokens

Here's an example of adding JWT authentication to the Next.js example:

import { verify } from 'jsonwebtoken';
export async function POST(request: NextRequest) {
try {
// Extract and verify JWT from Authorization header
const BEARER_TOKEN_PREFIX = 'Bearer ';
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith(BEARER_TOKEN_PREFIX)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const token = authHeader.substring(BEARER_TOKEN_PREFIX.length);
const user = verify(token, process.env.JWT_SECRET!) as { userId: string };
// Continue with token generation...
const body = await request.json();
// ...
} catch (error) {
// Continue with existing error handling logic here
}
}