Prove who you are to TeleQuick at two levels: your server authenticates with a long-lived API key, and each end user or session connects with a short-lived token you mint from that key. This keeps powerful credentials on your backend and hands out only narrowly-scoped access to clients. Two auth surfaces cover the two kinds of traffic:
  • Control plane (control-plane API tRPC: voice control, streams CRUD, agents, webhooks, analytics) — long-lived API keys scoped to an org and a set of capabilities.
  • Data plane (MoQT publish / subscribe, audio bridge, robotics topics, games rooms, data pub/sub) — short-lived relay tokens scoped to a set of namespaces, verified by the relay’s namespace_auth hook on every publish or subscribe.
The two are linked: a client uses its API key to mint relay tokens scoped to exactly the namespaces it needs.

Authenticate your server: API keys

Give your backend an API key so it can call the control plane. Create one in the dashboard or via streams.apiKeys.create({ orgId, label, scopes }). The key string is returned once, at creation; the control-plane API stores only the hash.
export TELEQUICK_API_KEY=tqs_live_abc123
Every control-plane API call (voice originate, streams create, webhook list, analytics, agent attach) authenticates with this key over HTTPS. The control-plane API rejects keys that don’t have the right capability scope for the procedure being called.

Authenticate a session: relay tokens

To let a client publish or subscribe on the data plane, mint it a relay token instead of sharing your API key. The relay never sees the API key. The control-plane API mints a short-lived relay token — a signed JWT carrying namespace claims — and the client presents that during the MoQT handshake. The relay’s namespace_auth hook verifies the JWT signature, extracts the namespace scope, and gates every subsequent publish / subscribe against it. A typical relay token looks like:
{
  "alg": "EdDSA",
  "kid": "rk_live_abc…",
  "iss": "telequick-bff",
  "sub": "tenant_abc",
  "ns":  ["voice/sid_xyz/*", "playback/li_xyz"],
  "exp": 1733900000
}
ClaimMeaning
kidSigning-key id; the relay fetches the matching public key from Redis.
issThe control-plane API that minted the token.
subTenant id the relay stamps on the session.
nsNamespace patterns this token may publish or subscribe to.
expExpiry (typically 1h for client tokens, longer for service-to-service).
Every modality client knows which namespaces it needs and asks the control-plane API for an appropriately scoped token. You don’t construct these JWTs by hand — the SDK’s modality clients call the control-plane API’s token-mint procedures for you when given a control-plane API key.

Authenticate an end user in the browser

In the browser, you cannot ship a long-lived API key. Instead, mint a short-lived relay token on your server for each browser session and pass it to the SPA:
// server (your control-plane API)
const token = await streams.signingKeys.mintPlaybackToken({
  inputId: "li_xyz", scopes: ["playback/li_xyz"], ttlSeconds: 3600,
});

// browser
const viewer = await BroadcastViewer.open(signedUrl);
For voice in a browser:
// server
const token = await voice.audioBridge.mintBrowserToken({
  callSid: "sid_xyz", ttlSeconds: 1800,
});
// browser
const v = new Voice({ baseUrl: controlPlaneApi, browserToken: token });
browserToken skips the API-key-on-client antipattern; the SDK presents it directly to the relay.

Rotate keys without downtime

Every relay token is signed with a per-org signing key. Rotate keys without downtime by:
  1. streams.signingKeys.create({ orgId, label }) — issues a new active key
  2. wait for clients with old tokens to expire (1h by default)
  3. streams.signingKeys.retire({ id }) — old key stops minting new tokens
The relay hydrates the active set of public keys from Redis on a 30 s refresh, so retirements take effect within that window.

Legacy: service-account JWT

The original control plane (the TeleQuickClient root import — dial, hangup, barge, push_audio) authenticated with an RSA service-account JSON file pointed at by TELEQUICK_CREDENTIALS:
{
  "tenant_id": "acme",
  "private_key": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----",
  "private_key_id": "kid-2026-01"
}
The SDK signed an RS256 JWT (iss=telequick-sdk, sub=tenant_id) and presented it on the QUIC handshake. This still works for backwards compat — new code should use API keys + relay tokens via the Voice modality.