Credentials
Switchboard hands out five kinds of credential. Most apps need only the first three.
| Looks like | Who holds it | Used for | Lasts | |
|---|---|---|---|---|
| API key | sb_... |
Your backend | Managing your Organization through the Switchboard API | Until you revoke it, or an expiry you set |
| Signing key | a JWK, kid acme_backend |
Your backend | Minting Client tokens yourself | Until you delete it |
| Client token | a JWT | A publisher or viewer | Connecting to a relay | An hour by default |
| Relay enrollment token | sbe_... |
You, briefly | Enrolling a Self-hosted relay | Used once, expires within a day |
| Node key | sbn_... |
Your Self-hosted relay | Talking to Switchboard and its peers | Until you delete the Node |
You use the API key to manage things, such as creating signing keys and enrolling relays. You use the signing key for the job you do constantly, which is minting a Client token for each publisher and viewer.
Only a Client token belongs in a browser or a mobile app. Keep the others on your servers.
An AI agent can also call the API with an access token it gets from WorkOS AuthKit, without you handing it any of these. See AI agents.
The examples on this page use Organization 4 and its Namespace acme. See
Organizations and Namespaces for what those are and how
API paths name them.
API keys
Section titled “API keys”An API key authenticates your calls to the Switchboard API. Send it as a bearer token:
curl https://app.caprid.com/v1/account \ -H "Authorization: Bearer $SB_API_KEY"You get one when you sign up. To make more, use your Organization’s page in
the dashboard, or send POST /v1/organizations/{id}/api-keys. The secret is
shown once, when the key is created. After that you see only its first few
characters.
An API key belongs to the Organization, not to the person who made it, so it keeps working when that person leaves. It reaches its own Organization and everything in it, and nothing else.
You should treat an API key like a password, because it can create signing keys. Relays never accept an API key, so a leaked key can’t be used to watch or publish directly. It can be used to mint tokens that can.
Limit an API key to some Namespaces
Section titled “Limit an API key to some Namespaces”If you keep prod and staging in separate Namespaces, your staging CI should
hold a key that can’t read prod’s signing keys. You can limit an API key to
some of your Organization’s Namespaces when you create it, in the dashboard or
by sending namespaces:
curl https://app.caprid.com/v1/organizations/4/api-keys \ -H "Authorization: Bearer $SB_API_KEY" \ -d '{"label": "staging-ci", "namespaces": ["acme-staging"]}'The response is the new key, with its limit:
{ "id": "3f9a1c07be42", "label": "staging-ci", "preview": "R2x0Z9", "created_at": 1767225600, "namespaces": ["acme-staging"], "secret": "sb_R2x0Z9..."}This key:
- can create, read and delete signing keys in
acme-staging - can mint and verify Client tokens in
acme-staging - can turn
acme-staging’splatform_peeringsetting on or off - gets a 403 for anything in
acmeor any other Namespace - gets a 403 for every
/v1/organizations/{id}route, which covers relays, enrollments and API keys
Because it can’t create API keys, the key can’t make itself a new key without the limit.
A limited key can’t list your Organization’s Namespaces, but it can look
itself up. GET /v1/account returns the key as api_key, limit included:
curl https://app.caprid.com/v1/account \ -H "Authorization: Bearer $SB_STAGING_KEY"The response is:
{ "account": { "api_key": { "id": "3f9a1c07be42", "label": "staging-ci", "preview": "R2x0Z9", "created_at": 1767225600, "last_used_at": 1767312000, "namespaces": ["acme-staging"] }, "organizations": [{"id": 4, "name": "Acme", "created_at": 1760000000}], "namespace": "acme-staging", "platform_peering": false }}namespace and platform_peering appear only because this key reaches
exactly one Namespace. A key with no limit has no namespaces in its
api_key.
You can’t change a key’s limit later. Create a new key instead. If you delete
a Namespace, it drops out of every key’s limit. A key whose Namespaces have
all been deleted shows "namespaces": [] and can’t do anything, so you should
revoke it.
Routes from before Organizations
Section titled “Routes from before Organizations”Scripts written before Organizations existed use routes without an
Organization or a Namespace in them: /v1/keys, /v1/tokens,
/v1/enrollments, /v1/nodes and /v1/account/api-keys. These routes still
work with an API key, and act in the key’s Organization.
/v1/keys and /v1/tokens work only while the key reaches exactly one
Namespace, because otherwise they can’t tell which Namespace you mean. That’s
the Organization’s only Namespace, or the only one in the key’s limit. A
limited key gets a 403 from the other old routes, because they act on the
whole Organization.
Move to the new routes when you next touch those scripts.
Signing keys
Section titled “Signing keys”A signing key mints Client tokens. You can also mint Client tokens through the API, but with a signing key, your own server-side code mints them directly, without a call to Switchboard for each one.
To create a signing key:
curl -X POST https://app.caprid.com/v1/namespaces/acme/keys \ -H "Authorization: Bearer $SB_API_KEY" \ -d '{"label": "backend"}'The response is the key:
{ "alg": "HS256", "key_ops": ["sign", "verify"], "kty": "oct", "k": "5mT0...", "kid": "acme_backend", "scope": {"root": "acme", "put": [""], "get": [""]}}k is the secret. The kid is your Namespace and the label, joined with an
underscore.
scope is the most the key can ever sign. By default, that’s your whole
Namespace, for publishing (put) and subscribing (get). You can narrow the
scope when you create the key, and you can’t widen it afterwards:
# Signs only tokens rooted under acme/customer-foo, for either role-d '{"label": "customer-foo", "path": "customer-foo"}'
# Signs only tokens that publish, and only under acme/ingest-d '{"label": "ingest", "publish": "ingest"}'If you name publish and not subscribe, the key can’t sign tokens that
subscribe. The same goes the other way round.
Relays enforce the scope every time they check a token, not only when the token is signed. So someone who gets hold of a signing key can’t reach outside its scope, but they can do anything inside it. You should keep signing keys on your servers.
Because the scope holds no matter who has the key, you can give a narrowly
scoped key to someone else. Suppose your customers run their own backends. A
key scoped to acme/customer-foo lets customer-foo mint tokens for their
own viewers, and they can’t touch any other customer’s broadcasts. They can
still do anything inside that scope, so the key is theirs to protect.
Client tokens
Section titled “Client tokens”A Client token is what a publisher or viewer presents to a relay. It’s a JWT with three claims that matter:
root: the path the token is anchored at. It’s the full path, so it starts with your Namespace.put: the prefixes underrootthat the holder may publish to.get: the prefixes underrootthat the holder may subscribe to.
"" in put or get means everything under root. Prefixes match whole
path segments, so live covers live/cam1 but not lively.
The Client passes the token as jwt= in the URL it connects to. For Mux’s
relays, that’s relay.mux.global, which sends each Client to the nearest
region:
https://relay.mux.global/acme/room/42?jwt=eyJhbGciOiJIUzI1NiIs...A Client of a Self-hosted relay uses that relay’s URL instead.
Mint one with the API
Section titled “Mint one with the API”Minting through the API takes one request per token. That’s fine for testing or for low volume:
curl -X POST https://app.caprid.com/v1/namespaces/acme/tokens \ -H "Authorization: Bearer $SB_API_KEY" \ -d '{"kid": "acme_backend", "path": "room/42", "subscribe": ""}'The response has the token and the claims inside it. root includes your
Namespace:
{ "token": "eyJhbGciOiJIUzI1NiIs...", "kid": "acme_backend", "claims": {"root": "acme/room/42", "get": [""], "exp": 1790301600, "iat": 1790298000}}A token grants nothing unless you ask for it, so you have to name publish,
subscribe, or both. The token expires after an hour unless you set
expires_in, in seconds. 0 means it never expires.
While the Namespace has exactly one signing key, you can leave out kid.
Sign your own
Section titled “Sign your own”In production, sign tokens in your backend. You save a round trip per viewer, and minting keeps working if Switchboard is unreachable.
A Client token is an ordinary HS256 JWT. Put the signing key’s kid in the
header, and use its k, base64url-decoded, as the secret. Here’s how with
jose in Node:
import { SignJWT, base64url } from "jose";
const key = JSON.parse(process.env.SB_SIGNING_KEY); // the JWK from /v1/namespaces/acme/keys
export function viewerToken(room) { return new SignJWT({ root: `acme/room/${room}`, get: [""] }) .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: key.kid }) .setIssuedAt() .setExpirationTime("1h") .sign(base64url.decode(key.k));}A token from viewerToken("42"):
- can subscribe to anything under
acme/room/42 - can’t publish anything
- expires after an hour
Here, root starts with acme/ because nothing adds the Namespace for you,
the way the API does.
The claims have to fit inside the signing key’s scope. If a token asks for
more, the relay refuses it when a Client uses it. To check a token while you
build this, send it to POST /v1/namespaces/acme/tokens/verify.
Revoke tokens
Section titled “Revoke tokens”You can’t revoke a single Client token. Keep tokens short-lived, and issue a new one when a session needs to go on longer.
Deleting a signing key revokes every token it signed. Clients using those tokens are disconnected within about a minute.
To rotate a signing key without cutting anyone off:
- Create the new key.
- Switch your backend to the new key.
- Wait for the tokens signed with the old key to expire.
- Delete the old key.
Relay enrollment tokens and Node keys
Section titled “Relay enrollment tokens and Node keys”You need these only if you run your own relays. A Relay enrollment token is a
one-time code that you create with POST /v1/organizations/{id}/enrollments.
Your relay exchanges it for a Node key. The relay then uses the Node key to
talk to Switchboard and to the other relays it peers with. See
Running a Self-hosted relay.
Deleting a Node deletes its Node key. Switchboard refuses the relay on its next poll, and the relay has to be enrolled again, with a new token, to come back.