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:
| Type | Name | Value/Description |
|---|---|---|
| Method | POST | |
| Headers | Content-Type | application/json |
| Optional Body | room_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:
| Type | Name | Value/Description |
|---|---|---|
| Status Code | 201 (Created) | |
| Headers | Content-Type | application/json |
| Response Body | server_url | Room connection URL |
participant_token | Room connection token |
Use an endpoint-based TokenSource
This guide walks you through setting up a server to generate room connection credentials.
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 Gemfilegem '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 serverwarp = "0.3"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"tokio = { version = "1", features = ["full"] }composer require agence104/livekit-server-sdkCreate a new file named
development.envwith 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>Create a server to host an endpoint at
/getToken, following the token endpoint specification:// server.gopackage mainimport ("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 joinsroomName := body.RoomNameif 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 SDKif 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 datavar body TokenSourceRequest// Create a JSON decoder and decode the request body into the structerr := 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.jsimport 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 joinsconst 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 SDKconst 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 minutesttl: '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.rbrequire 'livekit'require 'sinatra'set :port, 3000def 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 joinsroom_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 SDKtoken.identity = body["participant_identity"] || "quickstart-identity";token.name = body["participant_name"] || "quickstart-username";if body["participant_metadata"] dotoken.metadata = body["participant_metadata"]endif body["participant_attributes"] dotoken.attributes = body["participant_attributes"]endtoken.to_jwtendget '/getToken' dorequest.body.rewind # in case someone already read itbody = JSON.parse(request.body.read)json { "server_url" => ENV['LIVEKIT_URL'], "participant_token" => create_token(body) }end# server.pyimport osfrom livekit import apifrom flask import Flaskapp = 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 joinsroom_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 SDKtoken = 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.rsuse 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 routelet 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 serverwarp::serve(create_token_route).run(([127, 0, 0, 1], 3000)).await;}// Token creation functionfn 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 joinslet 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 SDKtoken = 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 JSONif (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() ]);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.phpNoteSee the Tokens overview page for more information on how to generate tokens with custom permissions.
Consume your endpoint with a
TokenSource:import { Room, TokenSource } from 'livekit-client';// Create the TokenSourceconst tokenSource = TokenSource.endpoint("http://localhost:3000/getToken");// Generate a new tokenconst { serverUrl, participantToken } = await tokenSource.fetch({ roomName: "room name to join" });// Use the generated token to connect to a roomconst room = new Room();room.connect(serverUrl, participantToken);import { TokenSource } from 'livekit-client';import { useSession, SessionProvider } from '@livekit/components-react';// Create the TokenSourceconst 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 unmountsuseEffect(() => {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@mainstruct 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 TokenSourceconst 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 unmountsuseEffect(() => {session.start();return () => {session.end();};}, []);return (<SessionProvider session={session}>{/* render the rest of your application here */}</SessionProvider>)}