LiveKit docs › Building Frontends › Authentication › Token endpoints

---

# 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 [token server](https://docs.livekit.io/frontends/build/authentication/sandbox-token-server.md).

### Endpoint schema

Your endpoint reads these fields from the request body and encodes the relevant attributes into the token. For fields clients aren't allowed to set, return a 4xx status code.

#### Request

A `POST` request with a JSON body containing any of these fields:

- **`room_name`** _(string)_ (optional): Room to join. Created on first join if it doesn't exist.

- **`participant_identity`** _(string)_ (optional): Participant identity. Surfaces as `LocalParticipant.identity` on the client.

- **`participant_name`** _(string)_ (optional): Display name. Surfaces as `LocalParticipant.name` on the client.

- **`room_config`** _(RoomConfiguration)_ (optional): Room-level config including agent dispatch info. See [Tokens & grants](https://docs.livekit.io/frontends/reference/tokens-grants.md).

- **`participant_metadata`** _(string)_ (optional): Your app's payload. Surfaces as `participant.metadata` on the client.

- **`participant_attributes`** _(map<string, string>)_ (optional): Your app's payload. Surfaces as `participant.attributes` on the client.

> 💡 **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

A `201 Created` response with a JSON body containing these fields:

- **`server_url`** _(string)_: Room connection URL.

- **`participant_token`** _(string)_: Room 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](https://developer.mozilla.org/en-US/docs/Web/API/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**:

```go
// go.mod
module example_server

go 1.21

require (
  github.com/livekit/protocol v1.11.0
)

```

```shell
go mod init example_server
go get github.com/livekit/protocol

```

---

**Node.js**:

```shell
# yarn:
yarn add livekit-server-sdk

# npm:
npm install livekit-server-sdk --save

```

---

**Ruby**:

```ruby
# Add to your Gemfile
gem 'livekit-server-sdk'

```

---

**Python**:

```shell
uv add livekit-api fastapi uvicorn

```

---

**Rust**:

```toml
# 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"] }

```

---

**PHP**:

```shell
composer require agence104/livekit-server-sdk

```
2. Create a new file named `development.env` with your connection URL, API key, and secret:

```shell
export LIVEKIT_URL=%{wsURL}%
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:

**Go**:

```go
// 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))
}

```

---

**Node.js**:

```js
// 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}`);
});

```

---

**Ruby**:

```ruby
# 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

```

---

**Python**:

```python
# server.py
import os
from livekit import api
from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()

@app.post('/getToken')
async def get_token(request: Request):
    body = await request.json()

    token = api.AccessToken(os.environ['LIVEKIT_API_KEY'], os.environ['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 {
        'server_url': os.environ['LIVEKIT_URL'],
        'participant_token': token.to_jwt()
    }

if __name__ == '__main__':
    uvicorn.run(app, port=3000)

```

---

**Rust**:

```rust
// 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()
}

```

---

**PHP**:

```php
// 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:

**Go**:

```shell
$ source development.env
$ go run server.go

```

---

**Node.js**:

```shell
$ source development.env
$ node server.js

```

---

**Ruby**:

```shell
$ source development.env
$ ruby server.rb

```

---

**Python**:

```shell
$ source development.env
$ python server.py

```

---

**Rust**:

```shell
$ source development.env
$ cargo r src/main.rs

```

---

**PHP**:

```shell
$ source development.env
$ php server.php

```

> ℹ️ **Note**
> 
> See the [Tokens & grants](https://docs.livekit.io/frontends/reference/tokens-grants.md) page for more information on how to generate tokens with custom permissions.
5. Consume your endpoint with a `TokenSource`:

**JavaScript**:

```typescript
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);

```

---

**React**:

```typescript
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} />
        )
      })}
    </>
  )
}

```

---

**Swift**:

```swift
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")
            }
        }
    }
}

```

---

**Android**:

```kotlin
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"
                }
            )
        }
    }
}

```

---

**Flutter**:

```dart
import 'package:livekit_client/livekit_client.dart' as sdk;
import 'package:livekit_components/livekit_components.dart' as components;

class MyApp extends StatefulWidget {
  @override
  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);

  @override
  void dispose() {
    session.dispose();
    super.dispose();
  }

  @override
  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'),
            );
          }
        },
      ),
    );
  }
}

```

---

**React Native**:

```typescript
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.

**Next.js (App Router)**:

Complete example using Next.js App Router with TypeScript:

```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 }
    );
  }
}

```

---

**Django**:

Complete example using Django with REST framework:

```python
# 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`:

```python
from django.urls import path
from .views import get_token

urlpatterns = [
    path('api/token', get_token, name='get_token'),
]

```

---

**FastAPI**:

Complete example using FastAPI:

```python
# 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

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'
        )

```

---

**Ruby on Rails**:

Complete example using Ruby on Rails:

```ruby
# 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`:

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

```

---

**Spring Boot (Java)**:

Complete example using Spring Boot:

```java
// 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:

```typescript
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
  }
}

```

---

This document was rendered at 2026-06-07T11:34:27.086Z.
For the latest version of this document, see [https://docs.livekit.io/frontends/build/authentication/endpoint.md](https://docs.livekit.io/frontends/build/authentication/endpoint.md).

To explore all LiveKit documentation, see [llms.txt](https://docs.livekit.io/llms.txt).