- Control plane (control-plane API tRPC: voice control, streams CRUD, agents, webhooks, analytics). This uses 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). This uses short-lived relay tokens scoped to a set of namespaces. The relay’s namespace_auth hook checks each token on every publish or subscribe.
Authenticate your server: API keys
Give your backend an API key so that it can call the control plane. Create one in the dashboard or withstreams.apiKeys.create({ orgId, label, scopes }). The API returns the
key string once, at creation. The control-plane API stores only the
hash.
Authenticate a session: relay tokens
To let a client publish or subscribe on the data plane, mint a relay token for it. Do not share your API key. The relay never sees the API key. The control-plane API mints a short-lived relay token. The token is a signed JWT that carries namespace claims. The client presents the token during the MoQT handshake. The relay’snamespace_auth hook
checks the JWT signature and extracts the namespace scope. It then gates
every subsequent publish / subscribe against that scope.
A typical relay token looks like:
Every modality client knows which namespaces it needs. It asks the
control-plane API for a token with that scope. You do not construct
these JWTs by hand. When you give the SDK a control-plane API key, its
modality clients call the control-plane API’s token-mint procedures for
you.
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. Pass the token to the SPA:browserToken avoids the API-key-on-client antipattern. The SDK
presents the token directly to the relay.
Rotate keys without downtime
A per-org signing key signs every relay token. To rotate keys without downtime:- Call
streams.signingKeys.create({ orgId, label }). This issues a new active key. - Wait until the clients’ old tokens expire (1h by default).
- Call
streams.signingKeys.retire({ id }). The old key then mints no new tokens.
Legacy: service-account JWT
The original control plane (theClutchCallClient root import:
dial, hangup, barge, push_audio) authenticated with an RSA
service-account JSON file. CLUTCHCALL_CREDENTIALS points at that file:
iss=clutchcall-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.
Related
- Modalities overview
- Architecture — where namespace_auth fits in the relay
- Streams reference —
apiKeys,signingKeys

