Access tokens and grants
What a join token carries, how long it lives, and why callers never assemble their own.
A join token is a short-lived signed statement: this identity, in this one room, with these permissions, until this moment. It is not a login and it carries no account.
What is in one#
{
"roomJoin": true,
"room": "support-8f3a21",
"canPublish": true,
"canSubscribe": true,
"canPublishData": false
}
Signed with the media server's own API key and secret — the same pair the server uses to verify webhooks, because LiveKit has one key pair per deployment and uses it in both directions.
Roles, and why you name one rather than building a grant#
A caller asks for a role by name. The kit decides what that role may do.
| Role | canPublish | canSubscribe | canPublishData |
|---|---|---|---|
human | true | true | false |
observer | false | true | false |
agent | true | true | true |
This is not ceremony. A caller that could assemble its own grant could hand a
browser participant canPublishData, and agent control messages ride the data
channel — a participant with that permission can drive the agent. The MCP
surface deliberately exposes no way to mint a token for an arbitrary grant, and
neither does the control plane API.
canPublishData is the one that bites#
An agent worker minted as human joins the room, appears in the participant
list, and never speaks. The control messages it sends are dropped, and nothing
reports it — not the worker, not the media server, not the browser. If an agent
is silently absent from its own call, check the role before anything else.
Lifetime#
Default one hour, ttl_seconds to change it.
The token is checked at join, not continuously — a participant already in a room is not ejected when their token expires. So a long TTL is not what keeps a long call alive, and a short one does not end it. Size the TTL for how long somebody might sit on a joining page, not for how long the call runs.
Minting one#
Three surfaces, same policy behind each:
# CLI
npx udk token create --room support-8f3a21 --identity daniel --role human --ttl 3600
# Control plane
curl -sX POST "$LIVEGRID_CONTROL_URL/token" \
-H "Authorization: Bearer $UDK_SECRET" \
-H 'content-type: application/json' \
-d '{"room":"support-8f3a21","identity":"daniel","role":"human","ttlSeconds":3600}'
// MCP
{ "name": "create_call_token",
"arguments": { "room": "support-8f3a21", "identity": "daniel", "role": "human" } }
All three return the same shape: token, url, room, identity, expiresAt.
When it will not mint#
503 with a message about credentials means LIVEKIT_API_KEY or
LIVEKIT_API_SECRET is unset where the control plane runs. It is deliberately a
503 rather than a 500: the service is fine, it is missing a dependency it cannot
supply itself. Errors