# Credentials

> API keys, signing keys, Client tokens and Node keys, and which one goes where.

URL: https://www.caprid.com/docs/guides/credentials/
Source: https://github.com/muxinc/switchboard/blob/main/docs/public/credentials.md

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](https://www.caprid.com/docs/guides/agents/).

The examples on this page use Organization `4` and its Namespace `acme`. See
[Organizations and Namespaces](https://www.caprid.com/docs/guides/organizations/) for what those are and how
API paths name them.

## API keys

An API key authenticates your calls to the Switchboard API. Send it as a
bearer token:

```bash
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

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`:

```bash
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:

```json
{
  "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`'s `platform_peering` setting on or off
- gets a 403 for anything in `acme` or 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:

```bash
curl https://app.caprid.com/v1/account \
  -H "Authorization: Bearer $SB_STAGING_KEY"
```

The response is:

```json
{
  "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

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

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:

```bash
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:

```json
{
  "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:

```bash
# 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

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 under `root` that the holder may publish to.
- `get`: the prefixes under `root` that 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

Minting through the API takes one request per token. That's fine for testing
or for low volume:

```bash
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:

```json
{
  "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

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`](https://github.com/panva/jose) in Node:

```js
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

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:

1. Create the new key.
2. Switch your backend to the new key.
3. Wait for the tokens signed with the old key to expire.
4. Delete the old key.

## 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](https://www.caprid.com/docs/guides/self-hosted-relays/).

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.
