ScriptsMay 11, 2026·4 min read

LiveKit Token Server — Sign JWTs for Room Access

Server-side token signing for LiveKit rooms. Python/Node SDKs. Per-user permissions, room scoping, TTL. Required for production.

Agent ready

Safe staging for this asset

This asset is staged first. The copied prompt tells the agent to inspect the staged files and ask before activating scripts, MCP config, or global config.

Stage only · 29/100Policy: stage
Agent surface
Any MCP/CLI agent
Kind
Skill
Install
Stage only
Trust
Trust: Community
Entrypoint
Asset
Safe staging command
npx -y tokrepo@latest install 793c4a17-cd72-40f3-aba5-9cb0f5957f1d --target codex

Stages files first; activation requires review of the staged README and plan.

Intro

LiveKit rooms require signed JWT access tokens — your backend signs a token with API key + secret, granting a specific identity permission to join a specific room with specific capabilities (publish, subscribe, record, data). Never sign in the browser. This is the server-side flow with Python and Node examples. Best for: any production LiveKit deployment, multi-tenant voice apps, per-user permission models. Works with: livekit-server-sdk (Python, Node, Go, Ruby, Java). Setup time: 10 minutes.


Python token endpoint (FastAPI)

from livekit import api
from fastapi import FastAPI, HTTPException, Depends
import os

app = FastAPI()

@app.post("/livekit/token")
async def issue_token(user_id: str, room: str, user = Depends(authenticated_user)):
    if not can_join_room(user, room):
        raise HTTPException(403, "not allowed in this room")

    token = (
        api.AccessToken(os.environ["LIVEKIT_API_KEY"], os.environ["LIVEKIT_API_SECRET"])
        .with_identity(user_id)
        .with_name(user.display_name)
        .with_grants(api.VideoGrants(
            room_join=True,
            room=room,
            can_publish=True,
            can_subscribe=True,
            can_publish_data=True,
        ))
        .with_ttl(timedelta(hours=1))
        .to_jwt()
    )
    return {"token": token, "url": os.environ["LIVEKIT_URL"]}

Node token endpoint (Express)

import { AccessToken } from 'livekit-server-sdk';

app.post('/livekit/token', requireAuth, async (req, res) => {
  const { userId, room } = req.body;
  if (!canJoinRoom(req.user, room)) return res.status(403).send();

  const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, {
    identity: userId,
    name: req.user.displayName,
    ttl: '1h',
  });
  at.addGrant({
    roomJoin: true,
    room,
    canPublish: true,
    canSubscribe: true,
    canPublishData: true,
  });

  res.json({ token: await at.toJwt(), url: process.env.LIVEKIT_URL });
});

Grant cheat sheet

Grant Use case
room_join Required for any participant
can_publish Participant can publish audio/video
can_subscribe Participant can hear/see others
can_publish_data Send arbitrary chat / state via DataChannel
room_admin Can mute, remove participants
room_record Can start egress recording
ingress_admin Can manage SIP / RTMP ingress

Production hardening

  • Short TTL — 1 hour max for end-user tokens. Browser refreshes via your endpoint.
  • Per-user identity — never reuse identities. user_id should be your DB primary key.
  • Server-side only — never log secrets, never ship API_SECRET to the browser.
  • Cache the SDK — instantiate once, reuse for many tokens.

FAQ

Q: Can I sign tokens in the browser? A: No — the API_SECRET would be exposed. Always sign in your backend. The browser only ever sees the resulting JWT, which has limited TTL and scoped grants.

Q: How do agents get their own token? A: Agents authenticate the same way — your worker process signs an agent identity token (with agent=True grant) before joining the room. LiveKit Cloud agent dispatchers handle this automatically when you deploy via the LiveKit CLI.

Q: What about webhooks for participant join/leave? A: Configure webhooks at cloud.livekit.io → Webhooks. Events: room_started, participant_joined, participant_left, track_published, egress_started. Verify the X-LiveKit-Signature header server-side.


Quick Use

  1. pip install livekit-api or npm install livekit-server-sdk
  2. Build an authenticated POST /livekit/token endpoint in your backend
  3. Frontend POSTs (user_id, room), gets back {token, url}, joins with the SDK

Intro

LiveKit rooms require signed JWT access tokens — your backend signs a token with API key + secret, granting a specific identity permission to join a specific room with specific capabilities (publish, subscribe, record, data). Never sign in the browser. This is the server-side flow with Python and Node examples. Best for: any production LiveKit deployment, multi-tenant voice apps, per-user permission models. Works with: livekit-server-sdk (Python, Node, Go, Ruby, Java). Setup time: 10 minutes.


Python token endpoint (FastAPI)

from livekit import api
from fastapi import FastAPI, HTTPException, Depends
import os

app = FastAPI()

@app.post("/livekit/token")
async def issue_token(user_id: str, room: str, user = Depends(authenticated_user)):
    if not can_join_room(user, room):
        raise HTTPException(403, "not allowed in this room")

    token = (
        api.AccessToken(os.environ["LIVEKIT_API_KEY"], os.environ["LIVEKIT_API_SECRET"])
        .with_identity(user_id)
        .with_name(user.display_name)
        .with_grants(api.VideoGrants(
            room_join=True,
            room=room,
            can_publish=True,
            can_subscribe=True,
            can_publish_data=True,
        ))
        .with_ttl(timedelta(hours=1))
        .to_jwt()
    )
    return {"token": token, "url": os.environ["LIVEKIT_URL"]}

Node token endpoint (Express)

import { AccessToken } from 'livekit-server-sdk';

app.post('/livekit/token', requireAuth, async (req, res) => {
  const { userId, room } = req.body;
  if (!canJoinRoom(req.user, room)) return res.status(403).send();

  const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, {
    identity: userId,
    name: req.user.displayName,
    ttl: '1h',
  });
  at.addGrant({
    roomJoin: true,
    room,
    canPublish: true,
    canSubscribe: true,
    canPublishData: true,
  });

  res.json({ token: await at.toJwt(), url: process.env.LIVEKIT_URL });
});

Grant cheat sheet

Grant Use case
room_join Required for any participant
can_publish Participant can publish audio/video
can_subscribe Participant can hear/see others
can_publish_data Send arbitrary chat / state via DataChannel
room_admin Can mute, remove participants
room_record Can start egress recording
ingress_admin Can manage SIP / RTMP ingress

Production hardening

  • Short TTL — 1 hour max for end-user tokens. Browser refreshes via your endpoint.
  • Per-user identity — never reuse identities. user_id should be your DB primary key.
  • Server-side only — never log secrets, never ship API_SECRET to the browser.
  • Cache the SDK — instantiate once, reuse for many tokens.

FAQ

Q: Can I sign tokens in the browser? A: No — the API_SECRET would be exposed. Always sign in your backend. The browser only ever sees the resulting JWT, which has limited TTL and scoped grants.

Q: How do agents get their own token? A: Agents authenticate the same way — your worker process signs an agent identity token (with agent=True grant) before joining the room. LiveKit Cloud agent dispatchers handle this automatically when you deploy via the LiveKit CLI.

Q: What about webhooks for participant join/leave? A: Configure webhooks at cloud.livekit.io → Webhooks. Events: room_started, participant_joined, participant_left, track_published, egress_started. Verify the X-LiveKit-Signature header server-side.


Source & Thanks

Built by LiveKit. Licensed under Apache-2.0.

livekit/python-sdks, livekit/server-sdk-js

🙏

Source & Thanks

Built by LiveKit. Licensed under Apache-2.0.

livekit/python-sdks, livekit/server-sdk-js

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets