Skip to main content

Endpoint token generation

Implement a LiveKit standardized token endpoint.

Overview

For a frontend or mobile app to connect to a LiveKit room, it needs a server URL pointing to your LiveKit Cloud project or self-hosted SFU instance, and a token generated by your backend server.

Endpoint token generation is an alternative to sandbox token generation. Use it when you can't use a sandbox token server or when you're ready to deploy your app to production. By following the standard endpoint format, you can use an endpoint-type TokenSource to integrate this same token generation endpoint into all of your applications.

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.

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.

  1. Install the LiveKit Server SDK:

    go get github.com/livekit/server-sdk-go/v2
    # 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: room,
    }
    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';
    const app = express();
    const port = 3000;
    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';
    const roomConfig = body.room_config ?? {};
    // 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 });
    at.roomConfig = roomConfig;
    const participantToken = await at.toJwt();
    res.send({ serverURL: process.env.LIVEKIT_URL, participantToken });
    });
    app.listen(port, () => {
    console.log(`Server listening on port ${port}`);
    });
    # server.rb
    require 'livekit'
    require 'sinatra'
    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"] do
    token.metadata = body["participant_metadata"]
    end
    if body["participant_attributes"] do
    token.attributes = body["participant_attributes"]
    end
    token.to_jwt
    end
    get '/getToken' do
    request.body.rewind # in case someone already read it
    body = JSON.parse(request.body.read)
    json { "server_url" => ENV['LIVEKIT_URL'], "participant_token" => create_token(body) }
    end
    # server.py
    import os
    from livekit import api
    from flask import Flask
    app = Flask(__name__)
    @app.route('/getToken', method=['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['room_name'] || 'quickstart-room'
    token.add_grant(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['participant_identity'] || 'quickstart-identity')
    .with_name(body['participant_name'] || '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 { 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;
    #[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("create-token")
    .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.get('room_name').unwrap_or("quickstart-room");
    token = token.with_grants(access_token::VideoGrants {
    room_join: true,
    room: room_name,
    ..Default::default()
    });
    if let Some(room_config) = body.get('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.get("participant_identity").unwrap_or("quickstart-identity"))
    .with_name(body.get("participant_name").unwrap_or("quickstart-username"));
    if let Some(participant_metadata) = body.get('participant_metadata') {
    token = token.with_metadata(participant_metadata)
    };
    if let Some(participant_attributes) = body.get('participant_attributes') {
    token = token.with_attributes(participant_attributes)
    };
    token.to_jwt()
    }
    // Left as an exercise to the reader: Make sure this is running on port 3000.
    // 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' => os.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 overview 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
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken");
    // 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
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken");
    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
    @main
    struct SessionApp: App {
    let session = Session(tokenSource: EndpointTokenSource(url: "http://localhost:3000/getToken"))
    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
    @State var message = ""
    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")).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;
    final tokenSource = sdk.EndpointTokenSource(url: "http://localhost:3000/getToken");
    final session = sdk.Session.fromConfigurableTokenSource(
    tokenSource,
    const TokenRequestOptions()
    );
    /* ... */
    await session.start();
    // Use session to further build out your application.
    import { TokenSource } from 'livekit-client';
    import { useSession, SessionProvider } from '@livekit/components-react';
    // Create the TokenSource
    const tokenSource = TokenSource.endpoint("http://localhost:3000/getToken");
    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>
    )
    }