To join a call, a user needs a token: a short-lived pass that says "this user may join this room, with these rights, until this time". Your backend makes the token with your API key. Your app only ever sees the token, never the key.
This page is for the backend half of a call. It answers three questions: who can join, what they can do once inside, and how to stop them.
Before you start: you need an API key on your backend. See API credentials.
How a token moves
1. alice clicks "Join call" in your app
2. Your app calls YOUR backend (e.g. POST /join-room)
3. Your backend checks alice is logged in and allowed in this room
4. Your backend calls livqeno.tokens.create(...) ← uses the API key
5. Livqeno returns a grant: { token, endpoint, iceServers, ... }
6. Your backend sends the grant to your app
7. Your app calls join() with it
Step 3 is yours. Livqeno doesn't know your users; it trusts your backend to decide who gets a token.
Make a token
On your backend, inside the endpoint your app calls when a user joins:
const token = await livqeno.tokens.create({
room: 'demo-room',
identity: req.user.id, // from YOUR session, never from the client
permissions: { join: true, publish: true, subscribe: true },
expiresIn: 3600, // optional, seconds
});token = livqeno.tokens.create(
CreateTokenParams(
room="demo-room",
identity=request.user.id, # from YOUR session, never from the client
permissions=TokenPermissions(join=True, publish=True, subscribe=True),
expires_in=3600, # optional, seconds
)
)token, err := client.Tokens.Create(ctx, livqeno.CreateTokenParams{
Room: roomID,
Identity: user.ID, // from YOUR session, never from the client
Permissions: &livqeno.TokenPermissions{
Join: livqeno.Bool(true), Publish: livqeno.Bool(true), Subscribe: livqeno.Bool(true),
},
ExpiresIn: 3600, // optional, seconds
})IssuedToken token = livqeno.tokens().create(CreateTokenParams.builder()
.room(roomId)
.identity(user.getId()) // from YOUR session, never from the client
.permissions(TokenPermissions.builder().join(true).publish(true).subscribe(true).build())
.expiresIn(3600) // optional, seconds
.build());$token = $client->tokens->create([
'room' => $roomId,
'identity' => $currentUser->id, // from YOUR session, never from the client
'permissions' => ['join' => true, 'publish' => true, 'subscribe' => true],
'expiresIn' => 3600, // optional, seconds
]);You get back { token, endpoint, iceServers, expiresAt, ... } (the
grant). Send the whole object to your app, unchanged. Every client
SDK's join() / Livqeno(...) expects exactly this. iceServers holds the
relay (TURN) details that let calls work on strict networks, and they're
made for this token only.
Permissions
Permissions decide what the user can do once they're in the room. Anything you don't turn on is off.
| Permission | Lets the user… |
|---|---|
join | Enter the room. |
publish | Send camera, mic, and screen share. |
subscribe | See and hear other people. |
publishData | Send data messages (Rooms & Participants). |
Real-world examples:
| Who | Permissions | Why |
|---|---|---|
| Doctor and patient in a video visit | join, publish, subscribe | Both talk and see each other. |
| Student in a lecture | join, subscribe | Watches and listens, can't send video. |
| Game lobby player who also sends game state | join, publish, subscribe, publishData | Adds data messages. |
- A viewer-only user gets
join+subscribe, withpublish: false. - You set permissions on the backend when you make the token.
- The app can't ask for more. Trying to publish without permission fails
with
PERMISSION_DENIED.
The server always checks
A user could edit your app's code in their browser. That's why the token is signed: the app can read it but can't change it.
The SDK reads the token in the app only to show friendly errors early
(like ROOM_NOT_FOUND). It doesn't trust what it reads.
The server checks every permission again when the user connects. A token that was changed or reused fails there.
Expiry
Every token expires. There are no tokens that last forever. By default the expiry only matters for joining: a user who joined with a valid token isn't kicked out when it expires mid-call. It matters again if they reconnect, and a server that enforces expiry ends the session when it runs out.
To keep long calls alive either way, give the SDK a refreshToken
callback that returns a new token from your backend (same room and
identity). The server warns the SDK a minute before expiry and the SDK
swaps the new token into the live session, with no reconnect. See
Keep long calls alive.
| Setting | Value |
|---|---|
| Set the lifetime with | expiresIn (SDK) or ttlSeconds (REST API) |
| Default | 10 minutes (stock configuration) |
| Maximum | 6 hours |
- Make a new token for each join. Don't reuse one across sessions.
- A short lifetime is your best protection if a token leaks.
- An old token fails with
TOKEN_EXPIRED. This is different fromINVALID_TOKEN, so your app can tell "get a fresh token" apart from "something is set up wrong".
Reusing a token
An RTC token is a bearer credential. Whoever holds a valid one can use it, as many times as they like, until it expires or you revoke it. There is no per-token join count.
What stops that from multiplying your usage is the identity rule, not the token:
- One token, one participant. A token is locked to one room and one
identity. Joining again with it, or with any token for the same identity in
that room, replaces the earlier connection. The older one is closed with
code
4002and leave reasonREPLACED, and it stops being metered. So a copied token can't run a crowd of simultaneous participants on your account. - The flip side is takeover. Someone holding a leaked token can join as that participant and push the real user out. They can keep doing it until the token expires or is revoked.
So treat a token like a password for one seat in one room:
- Mint it on your backend, per join, with the shortest
expiresInthat covers the call plus arefreshTokenfor long ones. See Expiry. - Never log it, put it in a URL you share, or store it past the join.
- If one leaks, revoke it (Revocation) and, to put the person
out right now, use
POST /v1/rooms/{roomId}/participants/{identity}/remove.
There is no concurrency cap beyond this. Expiry and revocation are the controls.
Revocation
You can kill a token before it expires. Use this when you gave a user a token and then changed your mind, for example they were removed from the meeting before they joined.
From your backend, with your API key:
DELETE /v1/rooms/{roomId}/rtc-tokens/{tokenId}tokenIdis theidfield from the mint response.- You can only revoke tokens from your own project and environment.
Anything else returns
404. - It's safe to call twice, or on a token that already expired.
What revocation does
It blocks the token for anything new. The next connection with it
fails with TOKEN_REVOKED. The SDK doesn't retry this error, because the
same token will never work again.
Revoking ends sessions running on the token
A user already in the call on that token is put out too: they get
room.left with reason TOKEN_REVOKED, an error with TOKEN_REVOKED,
and close code 4001. Their reconnect with the same token is refused.
To end a call that's running, close the room from your backend:
DELETE /v1/rooms/{roomId}This disconnects everyone in it.
Use revocation to stop future joins, for example when you gave a user a token and changed your mind. It helps, but a short token lifetime protects you more.
Each token is locked to one project and one room
These are signed into the token. Nobody can change them.
- Project and environment. A token only works in the project and environment that made it. A development token can't reach production.
- Room. A token works for exactly one room. If your app also sends a
room id when joining, it must match. A token for
room-acan never joinroom-b.
Permissions are signed too. The server enforces what's in the token, not what the app asks for. See Permissions.
Allowed browser origins are a separate setting per project. See Browser security & CORS.
For the token model shared by RTC and Chat, see Authentication → Tokens.
Common errors
| Error | Cause | Fix |
|---|---|---|
INVALID_TOKEN | Token is broken, or from a different project/environment. | Use the token exactly as tokens.create() returned it. |
TOKEN_EXPIRED | Past expiresAt. | Make a new one. See Expiry. |
ROOM_NOT_FOUND | The room passed to join() isn't the token's room. | Use the same room id/name you passed to tokens.create({ room }). |
TOKEN_REVOKED | The token was revoked with DELETE /v1/rooms/{roomId}/rtc-tokens/{tokenId}. | Make a new one. Retrying the same token never works. |
ORIGIN_NOT_ALLOWED | The page's address isn't on the project's allowed list. | Add it under Project Settings → Security → Allowed Origins. See Browser security. |
USAGE_LIMIT_EXCEEDED | The account used up its free RTC participant-minutes. | Retrying won't help. Calls already running keep going. See Usage. |
Checklist for production
- Make one token per join. Don't share one token across sessions.
- Set
identityfrom your backend's login session, never from the app. - Give only the permissions the user needs. Viewers always get
publish: false.
Next steps
Your backend can now make tokens. Next, use them in your app:
- Audio & Video: turn on the camera and mic (needs
publish). - Rooms & Participants: show who's in the call.
Related:
- Quickstart: the token used end to end.
- Rooms & Participants
- Authentication → Tokens: the model shared with Chat.
livqeno.tokens.create()reference: TypeScript, Python, Go, Java and PHP SDKs.