Skip to content

Access tokens ​

An access token is how your backend tells Talkline who a client is and what it may do. Your backend signs it with your API secret, in its own process, and hands it to the client; the client presents it when it connects to the signaling server.

There is no network call to mint one. That is deliberate: no latency on the path to a call, and calls still start if Talkline's control plane is down.

The rules ​

  • Single-use. The signaling server remembers each token's jti until it expires and refuses a second use with token_replayed. Reconnecting needs a new token too: it is what proves the identity again, so a stolen session id alone is worthless.
  • Short. exp − nbf is at most 600 seconds; a longer token is refused outright, not shortened — by the control API with token_ttl_too_long, and on connect as unauthenticated, which the SDK does not retry. A token only has to last until the connect — the session it opens outlives it.
  • Narrowed, never widened. Your application's settings can cap what any token grants. The client reads what it was actually given from client.grants, and should draw its buttons from that, never from what you asked for.
  • Fixed at the seat. Grants are frozen into a participant when it joins a room. Minting a wider token later does not change a participant already seated; PATCH /v1/rooms/{sid}/participants/{identity} does.
  • Narrowed on reconnect, never widened. The token a client reconnects with can only take away: from then on the connection has what both it and the new token grant, and a room set on the new token limits it to that room. A client in a room hears the change as an update to its own participant; client.grants keeps what it connected with. A wider token changes nothing until the client connects afresh. To take something away now, use PATCH /v1/rooms/{sid}/participants/{identity} or remove the participant.

Your token endpoint ​

Your client gets tokens from your backend, which already knows who its user is. The endpoint checks your own session, chooses the identity and grants, and mints:

ts
import { Talkline } from '@talkline/server-sdk';

const talkline = new Talkline({
  apiKey: process.env.TALKLINE_API_KEY!,
  apiSecret: process.env.TALKLINE_API_SECRET!,
  baseUrl: process.env.TALKLINE_API_URL!, // https://api.talkline.example
});

app.post('/api/talkline-token', requireSignedIn, async (req, res) => {
  const token = await talkline.mintToken({
    identity: `app:user:${req.user.id}`,
    name: req.user.displayName,
    ref: String(req.user.id),
    ttlSec: 300,
    grants: { roomJoin: true, roomCreate: true, canPublish: true, canSubscribe: true, canPublishData: true },
  });
  res.json({ token });
});

mintToken is also exported on its own, taking apiKey and apiSecret with the rest: import { mintToken } from '@talkline/server-sdk'. It makes no network call, so it cannot fail for a network reason: it throws an AuthError only for options no token may carry — a ttlSec outside 1–600 (token_ttl_too_long), an empty identity (unauthenticated), or metadata over 4 096 bytes (permission_denied). A throw is a bug in your endpoint; answer 500 and log its detail.

The client asks for a token every time it connects — on first connect and on every reconnect — so the endpoint must be quick, and must never return a token it has returned before. See the web SDK for the client side.

Claims ​

ClaimRequiredMeaning
issyesYour API key — how Talkline finds the secret to verify with.
subyesThe identity, up to 200 characters.
nbfyesNot before, Unix seconds. Set it to now.
expyesExpiry, Unix seconds; at most 600 after nbf.
jtiyesA unique id for this token — a random UUID. It is what makes the token single-use.
namenoThe display name others in a room see, at most 120 characters. POST /v1/tokens refuses a longer one; a token you sign yourself is not checked, and a longer name reaches every participant whole.
refnoYour own reference for the person — your user id — carried to the participant and the API, at most 120 characters. A guest has none.
metanoYour JSON metadata for the participant, at most 4 096 bytes.
grantsyesWhat the holder may do — below.

Signed HS256 with your API secret's UTF-8 bytes as the key. The verifier allows five seconds of clock skew.

Grants ​

GrantTypeMeaning
roomJoinbooleanMay join a room, and answer a call — answering is joining the call's room, so a token whose room names another room cannot answer. Without it, a client can connect but enter nothing.
roomstringA room sid (RM_…) or name. When set, the holder may join or create only that room.
roomCreatebooleanMay create a room — by joining a name nobody is using yet, or by placing a call, which creates one.
roomAdminbooleanMay mute and remove others, change their grants, change the room's metadata, and end the room.
roomListbooleanReserved: no request reads it yet.
canPublishbooleanMay send audio and video.
canSubscribebooleanMay receive others' audio and video.
canPublishDatabooleanMay send data messages to the room.
canPublishSourcesstring[]Which sources it may publish: microphone, camera, screen, screen_audio. Checked at every publish.
hiddenbooleanIn the room and invisible to everyone else — a supervisor. A hidden participant cannot publish, and cannot join a p2p room.
recorderbooleanReserved for the platform's recorder. Leave it off.
canUpdateOwnMetadatabooleanMay change its own metadata. Off by default, so a participant cannot rewrite what your backend said about it.

A grant left out is false. Common shapes:

ts
// Someone who places and answers calls.
{ roomJoin: true, roomCreate: true, canPublish: true, canSubscribe: true, canPublishData: true,
  canPublishSources: ['microphone', 'camera'] }

// A guest invited to one room by link, who may not create others.
{ roomJoin: true, room: 'RM_4f1c2a9e0b7d3c5a8e61', canPublish: true, canSubscribe: true,
  canPublishSources: ['microphone', 'camera'] }

// The host of a meeting: everything above, and admin of that room.
{ roomJoin: true, roomCreate: true, room: 'standup-team-7', roomAdmin: true, canPublish: true, canSubscribe: true,
  canPublishData: true, canPublishSources: ['microphone', 'camera', 'screen'] }

// A supervisor who listens in unseen.
{ roomJoin: true, room: 'RM_4f1c2a9e0b7d3c5a8e61', canSubscribe: true, hidden: true }

Minting in another language ​

The claim set above is the whole contract: any HS256 JWT library mints a valid token. In Python, with PyJWT:

python
import time, uuid, jwt

now = int(time.time())
token = jwt.encode(
    {
        "iss": TALKLINE_API_KEY,
        "sub": f"app:user:{user.id}",
        "nbf": now,
        "exp": now + 300,
        "jti": str(uuid.uuid4()),
        "name": user.display_name,
        "grants": {"roomJoin": True, "roomCreate": True, "canPublish": True, "canSubscribe": True},
    },
    TALKLINE_API_SECRET,
    algorithm="HS256",
)

In PHP, with firebase/php-jwt:

php
use Firebase\JWT\JWT;

$now = time();
$token = JWT::encode([
    'iss'    => $apiKey,
    'sub'    => "app:user:{$user->id}",
    'nbf'    => $now,
    'exp'    => $now + 300,
    'jti'    => bin2hex(random_bytes(16)),
    'name'   => $user->displayName,
    'grants' => ['roomJoin' => true, 'roomCreate' => true, 'canPublish' => true, 'canSubscribe' => true],
], $apiSecret, 'HS256');

Check a token minted this way with POST /v1/tokens/introspect before relying on it — remembering that introspection spends it.

Minting through the API ​

POST /v1/tokens mints a token on Talkline's side, for a backend that would rather not sign JWTs. It costs a network round trip on every connect — the thing local minting exists to avoid — so prefer minting locally.

ts
const { token, grants } = await talkline.tokens.mint({ identity: 'app:user:123', ttlSec: 300, grants: { roomJoin: true } });

Checking a token ​

POST /v1/tokens/introspect verifies a token exactly as the signaling server would and answers with its identity, the grants it asked for and the grants it would get. It spends the token, deliberately: a check that did not would be a way to test a token without using it, which the single-use rule exists to rule out.

Talkline documentation