You authenticate to ClutchCall at two levels. Your server authenticates with a long-lived API key. Each end user or session connects with a short-lived token that you mint from that key. This keeps powerful credentials on your backend. Clients get only narrowly-scoped access. Two auth surfaces cover the two kinds of traffic:
  • 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.
The two are linked. A client uses its API key to mint relay tokens scoped to exactly the namespaces that it needs.

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 with streams.apiKeys.create({ orgId, label, scopes }). The API returns the key string once, at creation. The control-plane API stores only the hash.
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 a key that does not have the correct capability scope for the called procedure.

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’s namespace_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:
For voice in a browser:
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:
  1. Call streams.signingKeys.create({ orgId, label }). This issues a new active key.
  2. Wait until the clients’ old tokens expire (1h by default).
  3. Call streams.signingKeys.retire({ id }). The old key then mints no new tokens.
The relay hydrates the active set of public keys from Redis on a 30 s refresh. Retirements take effect within that window.

Legacy: service-account JWT

The original control plane (the ClutchCallClient root import: dial, hangup, barge, push_audio) authenticated with an RSA service-account JSON file. CLUTCHCALL_CREDENTIALS points at that file:
The SDK signed an RS256 JWT (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.