# Switchboard Account API

OpenAPI 3.1.0, version 1. Machine-readable spec: https://www.caprid.com/openapi/account.yaml. Rendered reference: https://www.caprid.com/docs/reference/http/account/

Base URL: `https://app.caprid.com`

Manage your Organizations on Switchboard: their Namespaces, the Signing
keys and Client tokens for each Namespace, their API keys, and the
Self-hosted relays they enroll.

An Organization is a customer, and a Namespace is a path root it owns,
like an S3 bucket. Routes name what they act on. Everything that serves
the whole Organization (API keys, Nodes, enrollments) is under
`/v1/organizations/{id}`. Everything inside one Namespace (Signing keys,
Client tokens, its settings) is under `/v1/namespaces/{namespace}`. That
path needs no Organization, because Namespace names are unique.

Every request except signup, login, logout and the health check needs a
credential. Send an API key as `Authorization: Bearer sb_...`. An API key
belongs to one Organization and reaches only that Organization and its
Namespaces. You can limit a key to some of those Namespaces, so a staging
CI key can't read prod's Signing keys. A limited key gets a 403 outside
its Namespaces and on every `/v1/organizations/{id}` route. The dashboard
uses a session cookie instead, which reaches every Organization the
signed-in person is a Member of. Anything you can't reach is a 404,
whether or not it exists.

On a server that logs people in through WorkOS, such as
app.caprid.com, there are no passwords. You sign in at `/app/` in
a browser, and create API keys there for your scripts. On that server,
`POST /v1/login` returns 410, and `POST /v1/signup` works only as the
last step of signing in through the browser.

An AI agent can use the API without you creating an API key for it, on
a server set up for Agents. The Agent registers with WorkOS AuthKit, by
following the steps at `/auth.md`, and sends the access token AuthKit
gives it, its Agent token, as `Authorization: Bearer ...`. Once you claim the Agent at AuthKit, it acts
as your Account, so it can do anything your dashboard session can,
including creating API keys for itself. If you haven't signed up yet, the
Agent gets a 403 on every route until you do.

An Agent nobody has claimed can start right away. Its first request gives
it a Trial Organization with one Namespace, named `trial-` and a random
tag, so it can create Signing keys, mint Client tokens for Platform
relays, and create API keys for itself, with no person involved. A trial
has no Members. It can't enroll Self-hosted relays, create or delete a
Namespace, create an Organization, or manage Members and Invitations, and
those routes return 403 to the Agent and to the trial's API keys alike.
Starting trials is rate-limited per address, like signup, so an Agent's
first request can return 429.

The trial lapses 7 days after it starts, at its `trial_expires_at`. From
then on its API keys get a 401, and the Agent gets a 403 that says the
trial is over. Within about 10 minutes Switchboard deletes the trial's
Namespace and its Signing keys, so relays stop admitting its Client
tokens, and nobody can take the name again.

Claim the Agent before then to keep what it made. Its first request after
you claim it makes you the trial's Owner, and the Organization stops
being a trial. It no longer lapses, its limits lift, and its Namespace,
Signing keys and API keys keep working. It doesn't count against the
Organizations you may create. If you claim the Agent before you sign up,
the trial waits for you until it lapses. A trial that lapsed before the
claim stays lapsed.

Paths you send (`path`, `publish`, `subscribe`) are relative to the
Namespace, so `path: "room/42"` sent to `/v1/namespaces/acme/tokens` is
`acme/room/42`.

The routes from before Organizations (`/v1/keys`, `/v1/tokens`,
`/v1/enrollments`, `/v1/nodes`, `/v1/account/api-keys`, and `PATCH
/v1/account`) still work with an API key, acting in its Organization.
`/v1/keys`, `/v1/tokens` and `PATCH /v1/account` work only while the key
reaches exactly one Namespace: its Organization's only one, or the only
one in its limit. The others act on the whole Organization, so a limited
key gets a 403. With a session, they return 400 and name the route to use.

Request bodies are JSON. An empty body is the same as `{}`, and unknown
fields are ignored. Times are Unix seconds. Errors are always
`{"error": "..."}`, with a message meant for a person to read.

## Authentication

- **apiKey** (HTTP bearer): An API key, `sb_...`. Create one in the dashboard or with `POST /v1/organizations/{id}/api-keys`.
- **session** (cookie `__Host-switchboard_session`): The dashboard's session cookie, set by signup and login. Only accepted on same-origin requests, so it can't be used from another site. Over HTTPS it's named `__Host-switchboard_session`, which browsers accept only from this host, so no other host under the same domain can plant one. A server you run over plain HTTP names it `switchboard_session`.
- **agentToken** (HTTP bearer): The AuthKit access token an AI agent sends, on a server set up for Agents. The Agent registers with this server's WorkOS AuthKit domain and exchanges its assertion for the token, which lasts about five minutes. Until a person claims the Agent, the token acts in the Agent's Trial Organization. Once a person claims it, the token acts as that person's Account, with everything their session can do, and its first request makes them the trial's Owner. The server checks the token's signature against AuthKit's published keys, and that AuthKit issued it for this server's WorkOS client or for this server as a resource, the `resource` in `/.well-known/oauth-protected-resource`.

## Account

Signing up, logging in, and who you are.

### Show a signup in progress

```http
GET /v1/signup
```

operationId: `getPendingSignup` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/getpendingsignup/)

Only with WorkOS login. After a first sign-in, the person has proved
who they are but has no Account yet. This route returns the email
WorkOS verified, so the dashboard can ask for a Namespace. It also
lists the pending Invitations to that email, so the dashboard can
offer to join one of those Organizations instead. The login callback
sets a cookie that holds the signup in progress for 15 minutes.

**Responses**

- **200**: The signup in progress.
  - `application/json`: object
- **404**: No signup is in progress, or this server doesn't use WorkOS login.
  - `application/json`: [Error](#schema-error)

### Create an Account

```http
POST /v1/signup
```

operationId: `signup` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/signup/)

Creates an Account. Every Account starts by doing one of two things:

- **Creating an Organization.** Send `namespace`. The Organization is
  named after the Namespace (you can rename it later), you are its
  Owner, and the response carries its first API key. This is the
  only time that key's secret is shown, so store it now.
- **Joining one.** Send `invitation` instead, and you become a Member
  of the Organization that invited you, with the role it invited you
  as. You get no API key, because the Organization already has its
  own. Without WorkOS login, send the secret from the Invitation's
  link, because nothing has verified your email. With WorkOS login,
  send the Invitation's `id` from `GET /v1/signup`.

The response also sets a session cookie, for a signup that came from
the dashboard.

Password signups are limited to 20 an hour from each client address.

With WorkOS login, the body needs only `namespace` or `invitation`.
The email is the one WorkOS verified, there is no password, and the
request has to carry the cookie the login callback set.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) |  | Stored lowercased. `Alice@example.com` and `alice@example.com` are the same Account. |
| `password` | string |  | Length is the only rule. Counted in characters, not bytes. |
| `namespace` | [NamespaceName](#schema-namespacename) |  | A Namespace's name, which is the root of every path signed for it. It never changes. Use lowercase letters, digits and hyphens, and start and end with a letter or digit. Some names, like `admin` and `default`, are reserved, and so is every name that starts with `trial-`, which only Trial Organizations get. |
| `invitation` | string |  | Join the Organization this Invitation is for, instead of creating one. Send the secret from its link, or with WorkOS login, its `id`. Your email has to be the one invited. |

```json
{
  "email": "dev@acme.example",
  "password": "correct horse battery staple",
  "namespace": "acme"
}
```

**Responses**

- **201**: The Account, and when it created an Organization, that Organization's first API key with its secret.
  - `application/json`: object
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: With WorkOS login, there is no signup in progress. Sign in at `/app/` first.
  - `application/json`: [Error](#schema-error)
- **403**: The Invitation is for another email address.
  - `application/json`: [Error](#schema-error)
- **404**: No such Invitation.
  - `application/json`: [Error](#schema-error)
- **409**: The email or the Namespace is already taken.
  - `application/json`: [Error](#schema-error)
- **410**: The Invitation has already been used, was revoked, or has expired. Ask for a new one.
  - `application/json`: [Error](#schema-error)
- **429**: Rate limited. Try again later.
  - `application/json`: [Error](#schema-error)

### Sign in with WorkOS

```http
GET /v1/login
```

operationId: `startLogin` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/startlogin/)

Only with WorkOS login. Open this in a browser. It redirects to the
WorkOS sign-in page, which sends the browser back to
`/v1/login/callback`. The dashboard's sign-in button links here.

**Responses**

- **302**: Redirect to WorkOS.
- **404**: This server doesn't use WorkOS login.
  - `application/json`: [Error](#schema-error)

### Log in

```http
POST /v1/login
```

operationId: `login` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/login/)

Exchanges an email and password for a session cookie. Sessions last 14
days. Scripts should use an API key instead.

Failed logins are limited to 10 per 15 minutes, counted separately per
email and per client address.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) | yes |  |
| `password` | string | yes |  |

**Responses**

- **200**: Logged in.
  - `application/json`: [AccountEnvelope](#schema-accountenvelope)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: The email and password don't match an Account. The response is the same whether or not the email exists.
  - `application/json`: [Error](#schema-error)
- **410**: This server logs people in through WorkOS, and passwords no longer work.
  - `application/json`: [Error](#schema-error)
- **429**: Rate limited. Try again later.
  - `application/json`: [Error](#schema-error)

### Finish signing in with WorkOS

```http
GET /v1/login/callback
```

operationId: `finishLogin` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/finishlogin/)

WorkOS sends the browser back here. You never call it yourself.

A returning person gets a session cookie. A first sign-in whose
verified email matches an existing Account is linked to it, once and
for good. Anyone else gets a signup in progress, and the dashboard asks
them for a Namespace.

Either way the browser lands on `/app/`. When signing in didn't work,
`?login=` says why: `failed`, `unverified` (WorkOS hasn't verified the
email), or `conflict` (the email's Account is linked to a different
WorkOS user).

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `code` | query | string |  |  |
| `state` | query | string |  |  |

**Responses**

- **302**: Redirect to the dashboard.
- **404**: This server doesn't use WorkOS login.
  - `application/json`: [Error](#schema-error)

### Log out

```http
POST /v1/logout
```

operationId: `logout` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/logout/)

Ends the calling session and clears its cookie. Works with an expired
cookie, or none.

A session that came from WorkOS also has a WorkOS session to end, which
only the browser can do. The response says where to send it.

**Responses**

- **200**: Logged out here. Send the browser to `logout_url` to end the WorkOS session too.
  - `application/json`: object
- **204**: Logged out.

### Get the caller

```http
GET /v1/account
```

operationId: `getAccount` · [reference](https://www.caprid.com/docs/reference/http/account/operations/getaccount/)

Who you are and which Organizations you can manage. With a session,
that's every Organization you're a Member of. An Agent you claimed
gets the same answer as your session. With an API key, it's
the key's own Organization, and there's no email, because an API key
belongs to the Organization rather than to a person. Instead,
`api_key` describes the key itself, with the Namespaces it is limited
to.

**Responses**

- **200**: The caller.
  - `application/json`: [AccountEnvelope](#schema-accountenvelope)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent on every route with the token of an Agent that has nothing to act in. An Agent whose person hasn't signed up gets this until they sign up at `/app/`, and its next request after that acts as their new Account. An Agent nobody has claimed gets this once its trial lapses, because a registration gets one trial. It can register again to start a new one, or a person can claim it so it acts as their Account.
  - `application/json`: [Error](#schema-error)

## Organizations

The customers you manage, and their names.

### List your Organizations

```http
GET /v1/organizations
```

operationId: `listOrganizations` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listorganizations/)

Every Organization your session is a Member of, with your role in
each, oldest membership first. An API key lists only its own
Organization, with no role.

**Responses**

- **200**: The Organizations.
  - `application/json`: array of [Organization](#schema-organization)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent on every route with the token of an Agent that has nothing to act in. An Agent whose person hasn't signed up gets this until they sign up at `/app/`, and its next request after that acts as their new Account. An Agent nobody has claimed gets this once its trial lapses, because a registration gets one trial. It can register again to start a new one, or a person can claim it so it acts as their Account.
  - `application/json`: [Error](#schema-error)

### Create an Organization

```http
POST /v1/organizations
```

operationId: `createOrganization` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createorganization/)

Creates another Organization, the way signup created your first. Use
one to keep a separate customer, or a project you'll hand over, apart
from the rest. It has its own Namespaces, API keys, Members and
relays.

You become its Owner. It is named after its Namespace unless you give
it a `name`, which you can change later. The response holds its first
API key, and this is the only time that key's secret is shown, so
store it now.

This takes a signed-in session. An API key acts for its own
Organization, so it can't create another.

An Account may create 5 Organizations. Organizations you joined by
invitation don't count. If you need more, ask Mux to raise your cap.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `namespace` | [NamespaceName](#schema-namespacename) | yes | A Namespace's name, which is the root of every path signed for it. It never changes. Use lowercase letters, digits and hyphens, and start and end with a letter or digit. Some names, like `admin` and `default`, are reserved, and so is every name that starts with `trial-`, which only Trial Organizations get. |
| `name` | string |  | For display. Defaults to the Namespace. |

```json
{
  "namespace": "acme-labs",
  "name": "Acme Labs"
}
```

**Responses**

- **201**: The new Organization, its Namespace, and its first API key.
  - `application/json`: object
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key, or you have already created as many Organizations as you may. The message says which.
  - `application/json`: [Error](#schema-error)
- **409**: The Namespace is taken. Namespace names are never reused, even after one is deleted.
  - `application/json`: [Error](#schema-error)

### Get an Organization

```http
GET /v1/organizations/{id}
```

operationId: `getOrganization` · [reference](https://www.caprid.com/docs/reference/http/account/operations/getorganization/)

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The Organization.
  - `application/json`: [Organization](#schema-organization)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Rename an Organization

```http
PATCH /v1/organizations/{id}
```

operationId: `updateOrganization` · [reference](https://www.caprid.com/docs/reference/http/account/operations/updateorganization/)

The name is only for display, so any Member can change it, and so
can the Organization's API keys. Fields you leave out are left alone.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  | Leading and trailing spaces are trimmed. |

```json
{
  "name": "Acme, Inc."
}
```

**Responses**

- **200**: The Organization, renamed.
  - `application/json`: [Organization](#schema-organization)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Members

Who belongs to an Organization. Owners manage Members, signed in with a
session. Any Member can leave.

### List an Organization's Members

```http
GET /v1/organizations/{id}/members
```

operationId: `listMembers` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listmembers/)

Everyone who belongs to the Organization, with their role and when
they joined, longest-standing first. Any Member can list them, and so
can the Organization's API keys.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The Members.
  - `application/json`: array of [Member](#schema-member)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Remove a Member, or leave

```http
DELETE /v1/organizations/{id}/members/{account}
```

operationId: `deleteMember` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletemember/)

Owners can remove anyone, and any Member can remove themselves, which
is how you leave an Organization. Either way, it takes a session. An
API key can never manage Members.

The last Owner can't leave or be removed. Make someone else an Owner
first.

API keys the Member created keep working. They belong to the
Organization, so CI doesn't break when someone leaves. Revoke them
separately if you need to.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `account` | path | integer | yes | The Member's `account_id`, from the list. |

**Responses**

- **204**: Removed. The Account no longer sees the Organization.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner and this isn't you, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: You are removing the last Owner.
  - `application/json`: [Error](#schema-error)

### Change a Member's role

```http
PATCH /v1/organizations/{id}/members/{account}
```

operationId: `updateMember` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/updatemember/)

Owners only, signed in with a session. An API key can never manage
Members, so a leaked one can't be used to let its holder in.

An Organization always has an Owner. To step down as the last one,
make someone else an Owner first.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `account` | path | integer | yes | The Member's `account_id`, from the list. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | `"owner"` \| `"member"` | yes |  |

```json
{
  "role": "owner"
}
```

**Responses**

- **200**: The Member, with the new role.
  - `application/json`: [Member](#schema-member)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: This would leave the Organization without an Owner.
  - `application/json`: [Error](#schema-error)

## Invitations

How people join an Organization. An Owner invites an email address and
sends the link themselves. Only the person with that email can accept.

### List an Organization's pending Invitations

```http
GET /v1/organizations/{id}/invitations
```

operationId: `listInvitations` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listinvitations/)

Invitations that are still pending, oldest first. A pending
Invitation hasn't been accepted, revoked or expired. Their links aren't listed, because a link's secret is shown
only once, when you create the Invitation.

Owners only, signed in with a session.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The pending Invitations.
  - `application/json`: array of [Invitation](#schema-invitation)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner of this Organization, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Invite someone

```http
POST /v1/organizations/{id}/invitations
```

operationId: `createInvitation` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createinvitation/)

Invites one email address into the Organization, and returns a link
for that person. Switchboard sends no email, so copy the link and send
it yourself. Only the person with that email can use the link, so it's
safe if someone forwards it by mistake.

The link works once, and expires after 7 days. Its secret is shown
only in this response.

Owners only, signed in with a session. An API key can never invite
anyone, so a leaked one can't be used to let its holder in.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) | yes | Matched without regard to case. |
| `role` | `"owner"` \| `"member"` |  | The role they'll have once they accept. |

```json
{
  "email": "alice@acme.example",
  "role": "member"
}
```

**Responses**

- **201**: The Invitation, with its link.
  - `application/json`: [NewInvitation](#schema-newinvitation)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner of this Organization, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: That email already belongs to a Member.
  - `application/json`: [Error](#schema-error)

### Revoke an Invitation

```http
DELETE /v1/organizations/{id}/invitations/{invitation}
```

operationId: `revokeInvitation` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/revokeinvitation/)

Its link stops working. Owners only, signed in with a session.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `invitation` | path | string | yes | The Invitation's `id`, from the list. |

**Responses**

- **204**: Revoked.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner of this Organization, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such pending Invitation. One already accepted, revoked or expired is a 404 too.
  - `application/json`: [Error](#schema-error)

### List the Invitations to your email

```http
GET /v1/invitations
```

operationId: `listMyInvitations` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listmyinvitations/)

Only with WorkOS login, which has verified your email. Lists the
pending Invitations to it, from every Organization, so you can
accept one without its link. The dashboard shows them when you sign
up.

**Responses**

- **200**: The pending Invitations to your email.
  - `application/json`: array of [Invitation](#schema-invitation)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key, or with a session whose email WorkOS hasn't verified.
  - `application/json`: [Error](#schema-error)
- **404**: This server doesn't use WorkOS login.
  - `application/json`: [Error](#schema-error)

### Accept an Invitation

```http
POST /v1/invitations/accept
```

operationId: `acceptInvitation` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/acceptinvitation/)

Makes you a Member of the Organization, with the role the Invitation
names. Your email has to be the one invited, even if you have the
link. Each Invitation can be accepted once.

Send `secret`, the last part of the link (`/app/invitations/{secret}`).
With WorkOS login, which has verified your email, you can send the
Invitation's `id` instead. Without WorkOS, nothing has verified your
email, so only the link works.

This takes a session. An API key belongs to an Organization, not a
person.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `secret` | string |  | The secret from the Invitation's link. |
| `id` | string |  | The Invitation's id. Only with WorkOS login. |

```json
{
  "secret": "sbi_mR3x9Qe2vT8wY1zA4bC7dF0gH5jK6lN2pS8uV3xZ1qW"
}
```

**Responses**

- **200**: The Organization you joined.
  - `application/json`: [Organization](#schema-organization)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: The Invitation is for another email address, you sent an `id` on a server that hasn't verified your email, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such Invitation.
  - `application/json`: [Error](#schema-error)
- **409**: You're already a Member of that Organization.
  - `application/json`: [Error](#schema-error)
- **410**: The Invitation has already been used, was revoked, or has expired. Ask for a new one.
  - `application/json`: [Error](#schema-error)

## Namespaces

The path roots an Organization owns, and their settings.

### List an Organization's Namespaces

```http
GET /v1/organizations/{id}/namespaces
```

operationId: `listNamespaces` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listnamespaces/)

Oldest first.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The Namespaces.
  - `application/json`: array of [Namespace](#schema-namespace)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Create a Namespace

```http
POST /v1/organizations/{id}/namespaces
```

operationId: `createNamespace` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createnamespace/)

Gives the Organization another Namespace, such as `acme-staging`
beside `acme`, so that staging's Signing keys and Client tokens can't
reach prod. Any Member can create one, and so can an API key that
isn't limited to some Namespaces.

Names follow the same rules as at signup, and are unique across
Switchboard for good. A name that any Namespace has ever had,
including a deleted one, is never issued again.

An Organization can have 10 live Namespaces. Deleted ones don't
count. If you need more, ask Mux to raise the cap.

A new Namespace starts with `platform_peering` off.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `namespace` | [NamespaceName](#schema-namespacename) | yes | A Namespace's name, which is the root of every path signed for it. It never changes. Use lowercase letters, digits and hyphens, and start and end with a letter or digit. Some names, like `admin` and `default`, are reserved, and so is every name that starts with `trial-`, which only Trial Organizations get. |

```json
{
  "namespace": "acme-staging"
}
```

**Responses**

- **201**: The new Namespace.
  - `application/json`: [Namespace](#schema-namespace)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: The Organization already has as many Namespaces as it may, or you sent an API key limited to some Namespaces. The message says which.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: The name is taken, or was once.
  - `application/json`: [Error](#schema-error)

### Get a Namespace

```http
GET /v1/namespaces/{namespace}
```

operationId: `getNamespace` · [reference](https://www.caprid.com/docs/reference/http/account/operations/getnamespace/)

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Responses**

- **200**: The Namespace.
  - `application/json`: [Namespace](#schema-namespace)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Delete a Namespace

```http
DELETE /v1/namespaces/{namespace}
```

operationId: `deleteNamespace` · auth: `session` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletenamespace/)

Deletes the Namespace and its Signing keys. Client tokens they signed
stop working, and relays stop carrying the Namespace's tracks when
they next check a session, within about a minute.

This can't be undone, and the name can never be used again, by you
or anyone else.

Owners only, signed in with a session. An API key can never delete a
Namespace, so a leaked one can't destroy something you can't get
back.

Your Nodes, Regions and enrollments belong to the Organization and
stay as they are. You can delete the Organization's last Namespace.
The Organization stays, with no Namespaces, until you create another.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Responses**

- **204**: Deleted.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: You aren't an Owner, or you sent an API key.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Change a Namespace's settings

```http
PATCH /v1/namespaces/{namespace}
```

operationId: `updateNamespace` · [reference](https://www.caprid.com/docs/reference/http/account/operations/updatenamespace/)

Fields you leave out are left alone.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Request body**

Body `application/json`: [NamespaceSettings](#schema-namespacesettings)

```json
{
  "platform_peering": true
}
```

**Responses**

- **200**: The Namespace, with the change applied.
  - `application/json`: [Namespace](#schema-namespace)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## API keys

`sb_...` keys that authenticate calls to this API. A relay never
accepts one. A key can be limited to some of its Organization's
Namespaces when it is created.

### List API keys

```http
GET /v1/organizations/{id}/api-keys
```

operationId: `listAPIKeys` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listapikeys/)

Oldest first. Secrets are never listed, only a preview of each.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The Organization's API keys, expired ones included.
  - `application/json`: array of [APIKey](#schema-apikey)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Create an API key

```http
POST /v1/organizations/{id}/api-keys
```

operationId: `createAPIKey` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createapikey/)

The response is the only time the secret is shown. Switchboard keeps only a hash of it.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | [Label](#schema-label) |  | A name to tell the key apart from others. Defaults to `api`. |
| `expires_in` | integer |  | Seconds until the key stops working. `0` means never. |
| `namespaces` | array of [NamespaceName](#schema-namespacename) |  | Limits the key to these Namespaces, each a live Namespace of the Organization. Leave it out for a key that reaches the whole Organization. A limited key manages only the Signing keys, Client tokens and settings of its Namespaces, and can't create API keys. You can't change the limit later. |

```json
{
  "label": "staging-ci",
  "expires_in": 7776000,
  "namespaces": [
    "acme-staging"
  ]
}
```

**Responses**

- **201**: The new API key, with its secret.
  - `application/json`: [NewAPIKey](#schema-newapikey)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Revoke an API key

```http
DELETE /v1/organizations/{id}/api-keys/{key}
```

operationId: `deleteAPIKey` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deleteapikey/)

The key stops working on the next request that presents it.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `key` | path | string | yes | The key's `id` from the list. Not its secret. |

**Responses**

- **204**: Revoked.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Signing keys

Organization signing keys, which sign Client tokens. You can hold the secret
and sign Client tokens in your own backend without calling Switchboard.

### List Signing keys

```http
GET /v1/namespaces/{namespace}/keys
```

operationId: `listSigningKeys` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listsigningkeys/)

Oldest first, without their secrets. Get a key by its `kid` to see its secret.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Responses**

- **200**: The Namespace's Signing keys.
  - `application/json`: array of [SigningKeySummary](#schema-signingkeysummary)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Create a Signing key

```http
POST /v1/namespaces/{namespace}/keys
```

operationId: `createSigningKey` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createsigningkey/)

Creates an Organization signing key in this Namespace and returns it
as a JWK, secret included. Keep the secret in your backend to sign
Client tokens there without calling Switchboard.

The key's scope is fixed when you create it and can't be widened
later. By default, a key covers your whole Namespace for both
publishing and subscribing. Use `path` to root it lower down, and
`publish` or `subscribe` to limit what the tokens it signs may grant.
If you send only one of `publish` and `subscribe`, the key can't sign
tokens for the other role.

Relays look keys up by `kid` through Switchboard, never from the Client,
so a relay accepts a token signed with this key only within its scope.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | [Label](#schema-label) |  | Becomes part of the `kid`, which is `<namespace>_<label>`. Labels are unique within a Namespace. Defaults to a random 12-character hex string. |
| `path` | [Path](#schema-path) |  | A path relative to your Namespace, like `room/42`. Leading, trailing and repeated slashes are ignored. Segments can't be `.` or `..`, or contain whitespace or control characters. |
| `publish` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `subscribe` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |

```json
{
  "label": "backend"
}
```

**Responses**

- **201**: The new key, secret included.
  - `application/json`: [JWK](#schema-jwk)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: The Namespace already has a key with this label.
  - `application/json`: [Error](#schema-error)

### Get a Signing key

```http
GET /v1/namespaces/{namespace}/keys/{kid}
```

operationId: `getSigningKey` · [reference](https://www.caprid.com/docs/reference/http/account/operations/getsigningkey/)

Returns the full JWK, secret included. A `kid` that isn't one of this Namespace's is a 404.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |
| `kid` | path | string | yes | The key's `kid`, which is `<namespace>_<label>`. |

**Responses**

- **200**: The key.
  - `application/json`: [JWK](#schema-jwk)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Delete a Signing key

```http
DELETE /v1/namespaces/{namespace}/keys/{kid}
```

operationId: `deleteSigningKey` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletesigningkey/)

Every Client token the key signed stops working. Sessions already
running on those tokens end within about a minute, the next time
their relay checks them.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |
| `kid` | path | string | yes | The key's `kid`, which is `<namespace>_<label>`. |

**Responses**

- **204**: Deleted.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Client tokens

Tokens a Client presents to a relay to publish or subscribe. Minting one
here is a convenience. Anyone holding the Signing key can mint the same
token offline.

### Mint a Client token

```http
POST /v1/namespaces/{namespace}/tokens
```

operationId: `mintClientToken` · [reference](https://www.caprid.com/docs/reference/http/account/operations/mintclienttoken/)

Signs a Client token with one of the Namespace's Signing keys. Use it
when you'd rather not hold a Signing key's secret in your own backend.

A token gets no grants by default. Name what it may publish, subscribe
to, or both. A request that names neither is a 400. Whatever you ask
for has to fit inside the Signing key's scope.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `kid` | string |  | The `kid` of the Signing key to sign with. You can leave it out when the Namespace has exactly one. |
| `path` | [Path](#schema-path) |  | Where the token is rooted, relative to the Namespace. The token's `root` claim is `<namespace>/<path>`. |
| `publish` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `subscribe` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `expires_in` | integer |  | Seconds until the token expires. `0` means never. |

```json
{
  "path": "room/42",
  "subscribe": ""
}
```

**Responses**

- **200**: The signed token and what it grants.
  - `application/json`: [MintedToken](#schema-mintedtoken)
- **400**: The request can't be signed: it grants nothing, it asks for more than the key's scope, it reaches outside the Namespace, or it names no key while the Namespace has zero or several.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: The Namespace isn't one you can reach, or the `kid` isn't one of its keys.
  - `application/json`: [Error](#schema-error)

### Check a Client token

```http
POST /v1/namespaces/{namespace}/tokens/verify
```

operationId: `verifyClientToken` · [reference](https://www.caprid.com/docs/reference/http/account/operations/verifyclienttoken/)

Verifies a token against this Namespace's Signing keys and returns its
claims. Use it to debug a token a relay refuses. A token signed by any
other Namespace's key gets a 401.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | path | [NamespaceName](#schema-namespacename) | yes |  |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | yes |  |

**Responses**

- **200**: The token is valid.
  - `application/json`: [VerifiedToken](#schema-verifiedtoken)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: The token is malformed, expired, has a bad signature, or wasn't signed by one of this Namespace's keys. Also returned when the caller isn't authenticated.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Self-hosted relays

Enrolling relays you run yourself into your Organization, and removing
them. A Self-hosted relay carries all of its Organization's Namespaces.

### List pending enrollments

```http
GET /v1/organizations/{id}/enrollments
```

operationId: `listEnrollments` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listenrollments/)

Relay enrollment tokens that haven't been used or expired yet. Oldest first.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The pending enrollments.
  - `application/json`: array of [Enrollment](#schema-enrollment)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Create a Relay enrollment token

```http
POST /v1/organizations/{id}/enrollments
```

operationId: `createEnrollment` · [reference](https://www.caprid.com/docs/reference/http/account/operations/createenrollment/)

Issues a one-time token that a relay you run exchanges for its Node key,
with `POST /cluster/enroll`. You decide everything about the Node
here: its URL, its Region, whether it can be a Gateway, and its Client
URL. The relay can't change any of it later.

The token is shown once. It expires after `expires_in` seconds, or
when it's used, whichever comes first.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | yes | The URL peers will dial this relay on, and Clients too unless `client_url` names another. It needs a publicly trusted certificate for its host. |
| `region` | string |  | Which of your Regions the Node joins. Relays in the same Region all peer with each other. Region names belong to you, so your `iad` is unrelated to anyone else's. Can't contain `/`. |
| `gateway` | boolean |  | Whether this Node may be elected its Region's Gateway, which carries the Region's traffic to other Regions. |
| `client_url` | string (uri) |  | The Client URL, which is where Clients, such as a player page, reach this relay when that isn't `url`. Use it when `url` is a name only your private network resolves, and Clients come in through a public hostname or a load balancer. Switchboard doesn't route anything with it. It reports the value on the Node, for whatever hands relay addresses to your Clients. It has to start with `http://` or `https://` and can't contain a username or password. Switchboard trims it and stores it as you send it. Leave it out, or send `""`, for none. The relay can't change it. If the relay's `connect_api` URL carries `client=`, that value has to match. |
| `expires_in` | integer |  | Seconds until the token expires. `0` means the default. It always expires. |

```json
{
  "url": "https://relay1.internal.acme.example:4443/",
  "region": "iad",
  "gateway": true,
  "client_url": "https://live.acme.example/"
}
```

**Responses**

- **201**: The pending enrollment, with its token.
  - `application/json`: [NewEnrollment](#schema-newenrollment)
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)
- **409**: A Node already exists at this URL. Delete it first.
  - `application/json`: [Error](#schema-error)

### Revoke a pending enrollment

```http
DELETE /v1/organizations/{id}/enrollments/{enrollment}
```

operationId: `deleteEnrollment` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deleteenrollment/)

The token stops working. A Node that already enrolled with it stays enrolled.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `enrollment` | path | string | yes | The enrollment's `id`, from the list. Not the token. |

**Responses**

- **204**: Revoked.
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### List your Nodes

```http
GET /v1/organizations/{id}/nodes
```

operationId: `listNodes` · [reference](https://www.caprid.com/docs/reference/http/account/operations/listnodes/)

Every Node the Organization has enrolled, running or not.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |

**Responses**

- **200**: The Nodes.
  - `application/json`: array of [Node](#schema-node)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Delete a Node

```http
DELETE /v1/organizations/{id}/nodes
```

operationId: `deleteNode` · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletenode/)

Deletes the Node and its Node key. Its relay is refused from its next
poll, and its peers drop it. To bring the relay back, enroll it again
with a new Relay enrollment token.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | The Organization's `id`, from `GET /v1/organizations`. |
| `url` | query | string (uri) | yes | The Node's URL, as listed. |

**Responses**

- **204**: Deleted.
- **400**: The request is malformed or a field is invalid. The message says which.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Before Organizations

The routes from before Organizations, kept for API keys. Each one does
what a newer route does, in the API key's own Organization.

### Change the Namespace's settings

```http
PATCH /v1/account
```

operationId: `updateAccountLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/updateaccountlegacy/)

`PATCH /v1/namespaces/{namespace}` for the API key's only Namespace, answered with the caller.

**Request body**

Body `application/json`: [NamespaceSettings](#schema-namespacesettings)

**Responses**

- **200**: The caller, with the change applied.
  - `application/json`: [AccountEnvelope](#schema-accountenvelope)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### List API keys

```http
GET /v1/account/api-keys
```

operationId: `listAPIKeysLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/listapikeyslegacy/)

`GET /v1/organizations/{id}/api-keys` for the API key's Organization.

**Responses**

- **200**: The Organization's API keys.
  - `application/json`: array of [APIKey](#schema-apikey)
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### Create an API key

```http
POST /v1/account/api-keys
```

operationId: `createAPIKeyLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/createapikeylegacy/)

`POST /v1/organizations/{id}/api-keys` for the API key's Organization.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | [Label](#schema-label) |  | A name to tell the key apart from others. Defaults to `api`. |
| `expires_in` | integer |  | Seconds until the key stops working. `0` means never. |
| `namespaces` | array of [NamespaceName](#schema-namespacename) |  | Limits the key to these Namespaces, each a live Namespace of the Organization. Leave it out for a key that reaches the whole Organization. A limited key manages only the Signing keys, Client tokens and settings of its Namespaces, and can't create API keys. You can't change the limit later. |

```json
{
  "label": "staging-ci",
  "expires_in": 7776000,
  "namespaces": [
    "acme-staging"
  ]
}
```

**Responses**

- **201**: The new API key, with its secret.
  - `application/json`: [NewAPIKey](#schema-newapikey)
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### Revoke an API key

```http
DELETE /v1/account/api-keys/{key}
```

operationId: `deleteAPIKeyLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/deleteapikeylegacy/)

`DELETE /v1/organizations/{id}/api-keys/{key}` for the API key's Organization.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes |  |

**Responses**

- **204**: Revoked.
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### List Signing keys

```http
GET /v1/keys
```

operationId: `listSigningKeysLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/listsigningkeyslegacy/)

`GET /v1/namespaces/{namespace}/keys` for the API key's only Namespace.

**Responses**

- **200**: The Namespace's Signing keys.
  - `application/json`: array of [SigningKeySummary](#schema-signingkeysummary)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### Create a Signing key

```http
POST /v1/keys
```

operationId: `createSigningKeyLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/createsigningkeylegacy/)

`POST /v1/namespaces/{namespace}/keys` for the API key's only Namespace.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | [Label](#schema-label) |  | Becomes part of the `kid`, which is `<namespace>_<label>`. Labels are unique within a Namespace. Defaults to a random 12-character hex string. |
| `path` | [Path](#schema-path) |  | A path relative to your Namespace, like `room/42`. Leading, trailing and repeated slashes are ignored. Segments can't be `.` or `..`, or contain whitespace or control characters. |
| `publish` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `subscribe` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |

```json
{
  "label": "backend"
}
```

**Responses**

- **201**: The new key, secret included.
  - `application/json`: [JWK](#schema-jwk)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **409**: The Namespace already has a key with this label.
  - `application/json`: [Error](#schema-error)

### Get a Signing key

```http
GET /v1/keys/{kid}
```

operationId: `getSigningKeyLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/getsigningkeylegacy/)

`GET /v1/namespaces/{namespace}/keys/{kid}` for the API key's only Namespace.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `kid` | path | string | yes |  |

**Responses**

- **200**: The key.
  - `application/json`: [JWK](#schema-jwk)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Delete a Signing key

```http
DELETE /v1/keys/{kid}
```

operationId: `deleteSigningKeyLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletesigningkeylegacy/)

`DELETE /v1/namespaces/{namespace}/keys/{kid}` for the API key's only Namespace.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `kid` | path | string | yes |  |

**Responses**

- **204**: Deleted.
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Mint a Client token

```http
POST /v1/tokens
```

operationId: `mintClientTokenLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/mintclienttokenlegacy/)

`POST /v1/namespaces/{namespace}/tokens` for the API key's only Namespace.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `kid` | string |  | The `kid` of the Signing key to sign with. You can leave it out when the Namespace has exactly one. |
| `path` | [Path](#schema-path) |  | Where the token is rooted, relative to the Namespace. The token's `root` claim is `<namespace>/<path>`. |
| `publish` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `subscribe` | [Prefixes](#schema-prefixes) |  | Path prefixes, relative to `path`. `""` means everything under `path`. Matching is by whole segment: `live` covers `live/cam1` but not `lively`. A single string is the same as a one-item array. |
| `expires_in` | integer |  | Seconds until the token expires. `0` means never. |

```json
{
  "path": "room/42",
  "subscribe": ""
}
```

**Responses**

- **200**: The signed token and what it grants.
  - `application/json`: [MintedToken](#schema-mintedtoken)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### Check a Client token

```http
POST /v1/tokens/verify
```

operationId: `verifyClientTokenLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/verifyclienttokenlegacy/)

`POST /v1/namespaces/{namespace}/tokens/verify` for the API key's only Namespace.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | yes |  |

**Responses**

- **200**: The token is valid.
  - `application/json`: [VerifiedToken](#schema-verifiedtoken)
- **400**: Sent with a session, or with an API key that doesn't reach exactly one Namespace. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### List pending enrollments

```http
GET /v1/enrollments
```

operationId: `listEnrollmentsLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/listenrollmentslegacy/)

`GET /v1/organizations/{id}/enrollments` for the API key's Organization.

**Responses**

- **200**: The pending enrollments.
  - `application/json`: array of [Enrollment](#schema-enrollment)
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### Create a Relay enrollment token

```http
POST /v1/enrollments
```

operationId: `createEnrollmentLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/createenrollmentlegacy/)

`POST /v1/organizations/{id}/enrollments` for the API key's Organization.

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | yes | The URL peers will dial this relay on, and Clients too unless `client_url` names another. It needs a publicly trusted certificate for its host. |
| `region` | string |  | Which of your Regions the Node joins. Relays in the same Region all peer with each other. Region names belong to you, so your `iad` is unrelated to anyone else's. Can't contain `/`. |
| `gateway` | boolean |  | Whether this Node may be elected its Region's Gateway, which carries the Region's traffic to other Regions. |
| `client_url` | string (uri) |  | The Client URL, which is where Clients, such as a player page, reach this relay when that isn't `url`. Use it when `url` is a name only your private network resolves, and Clients come in through a public hostname or a load balancer. Switchboard doesn't route anything with it. It reports the value on the Node, for whatever hands relay addresses to your Clients. It has to start with `http://` or `https://` and can't contain a username or password. Switchboard trims it and stores it as you send it. Leave it out, or send `""`, for none. The relay can't change it. If the relay's `connect_api` URL carries `client=`, that value has to match. |
| `expires_in` | integer |  | Seconds until the token expires. `0` means the default. It always expires. |

```json
{
  "url": "https://relay1.internal.acme.example:4443/",
  "region": "iad",
  "gateway": true,
  "client_url": "https://live.acme.example/"
}
```

**Responses**

- **201**: The pending enrollment, with its token.
  - `application/json`: [NewEnrollment](#schema-newenrollment)
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **409**: A Node already exists at this URL. Delete it first.
  - `application/json`: [Error](#schema-error)

### Revoke a pending enrollment

```http
DELETE /v1/enrollments/{enrollment}
```

operationId: `deleteEnrollmentLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/deleteenrollmentlegacy/)

`DELETE /v1/organizations/{id}/enrollments/{enrollment}` for the API key's Organization.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `enrollment` | path | string | yes |  |

**Responses**

- **204**: Revoked.
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

### List your Nodes

```http
GET /v1/nodes
```

operationId: `listNodesLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/listnodeslegacy/)

`GET /v1/organizations/{id}/nodes` for the API key's Organization.

**Responses**

- **200**: The Nodes.
  - `application/json`: array of [Node](#schema-node)
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)

### Delete a Node

```http
DELETE /v1/nodes
```

operationId: `deleteNodeLegacy` · **deprecated** · [reference](https://www.caprid.com/docs/reference/http/account/operations/deletenodelegacy/)

`DELETE /v1/organizations/{id}/nodes` for the API key's Organization.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `url` | query | string (uri) | yes |  |

**Responses**

- **204**: Deleted.
- **400**: Sent with a session, which may belong to several Organizations. Use the route the message names instead. This route also returns 400 for anything that route would refuse with 400.
  - `application/json`: [Error](#schema-error)
- **401**: No valid credential. Missing, expired and revoked credentials all get the same response, and so does an API key whose trial has lapsed. An Agent token from before a claim gets a 401 too, once the claim has made the trial a person's, with a message saying to get a new token.
  - `application/json`: [Error](#schema-error)
- **403**: Sent with an API key limited to some Namespaces, to a Namespace outside its limit, or to a route that acts on the Organization as a whole. A limited key can never create API keys.
  - `application/json`: [Error](#schema-error)
- **404**: No such thing, or not yours. Another customer's Organization or Namespace is a 404 too, so nobody can tell which ones exist.
  - `application/json`: [Error](#schema-error)

## Agents

How an AI agent that finds this server learns where to register, on a
server set up for Agents. Neither route needs a credential.

### Instructions for an Agent

```http
GET /auth.md
```

operationId: `getAgentInstructions` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/getagentinstructions/)

The steps an AI agent follows to register with this server's WorkOS
AuthKit domain, get an Agent token, and use it here. Point an agent at
this page and it can start a trial with no help from you.

The first part is AuthKit's own instructions, copied from AuthKit at
most a minute ago, so they follow how AuthKit is set up. The last part
says what to call here with the token: list the trial, find its
Namespace, and create an API key.

**Responses**

- **200**: The instructions, as Markdown.
  - `text/plain`: string
- **404**: This server isn't set up for Agents.
  - `application/json`: [Error](#schema-error)
- **502**: AuthKit didn't answer, or answered with an error.
  - `application/json`: [Error](#schema-error)

### Where an Agent registers

```http
GET /.well-known/oauth-protected-resource
```

operationId: `getProtectedResourceMetadata` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/getprotectedresourcemetadata/)

This server's OAuth protected resource metadata (RFC 9728). It names
the WorkOS AuthKit domain where an Agent registers, so an agent or
OAuth client that finds this server can get an Agent token without
being told where. A 401 from any other route points here too, in its
`WWW-Authenticate` header.

`resource` is this server's public origin, the same whichever
hostname you fetched this from. This server accepts a token issued
for its WorkOS client, which is what an Agent following `/auth.md`
gets, or for exactly that resource (RFC 8707). AuthKit doesn't issue
the second kind: it answers `invalid_target` to a token request that
names a `resource`, whatever the URL, so leave it out.

**Responses**

- **200**: The metadata.
  - `application/json`: [ProtectedResource](#schema-protectedresource)
- **404**: This server isn't set up for Agents.
  - `application/json`: [Error](#schema-error)

## Health

Liveness, for load balancers and monitoring.

### Health check

```http
GET /health
```

operationId: `health` · auth: none · [reference](https://www.caprid.com/docs/reference/http/account/operations/health/)

**Responses**

- **200**: Up.
  - `application/json`: [Health](#schema-health)
- **503**: Up, but not ready to serve.
  - `application/json`: [Health](#schema-health)

## Schemas

<a id="schema-error"></a>

### Error

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string | yes |  |

<a id="schema-namespacename"></a>

### NamespaceName

A Namespace's name, which is the root of every path signed for it.
It never changes. Use lowercase letters, digits and hyphens, and start
and end with a letter or digit. Some names, like `admin` and
`default`, are reserved, and so is every name that starts with
`trial-`, which only Trial Organizations get.

Type: string

<a id="schema-label"></a>

### Label

Type: string

<a id="schema-path"></a>

### Path

A path relative to your Namespace, like `room/42`. Leading, trailing
and repeated slashes are ignored. Segments can't be `.` or `..`, or
contain whitespace or control characters.

Type: string

<a id="schema-prefixes"></a>

### Prefixes

Path prefixes, relative to `path`. `""` means everything under `path`.
Matching is by whole segment: `live` covers `live/cam1` but not
`lively`. A single string is the same as a one-item array.

Type: string \| array of string

<a id="schema-account"></a>

### Account

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) |  | The person signed in. Absent for an API key. |
| `created_at` | integer |  | When the person signed up. Absent for an API key. |
| `api_key` | [APIKey](#schema-apikey) |  | The API key making the call, with its `namespaces` limit if it has one. A limited key can't list its Organization's Namespaces, so this is how it finds the ones it can reach. Absent for a session. |
| `organizations` | array of [Organization](#schema-organization) | yes |  |
| `namespace` | [NamespaceName](#schema-namespacename) |  | The one Namespace you can reach, as this reported before Organizations. Absent unless you reach exactly one. |
| `platform_peering` | boolean |  | That Namespace's `platform_peering`. Absent along with `namespace`. |

<a id="schema-organization"></a>

### Organization

A customer of Switchboard. It owns its Namespaces, and the API keys,
Nodes and enrollments that serve them. Its Members manage all of it.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Identifies the Organization in routes. It never changes. |
| `name` | string | yes | For display. Any Member can change it. |
| `role` | `"owner"` \| `"member"` |  | Your role in it. Absent for an API key, which has none. |
| `created_at` | integer | yes |  |
| `trial_expires_at` | integer |  | When a Trial Organization lapses, so its Agent knows how long it has to get a person to claim it. Absent for every other Organization. |

<a id="schema-invitation"></a>

### Invitation

An offer of membership in an Organization to one email address.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the Invitation in routes. Not a secret. |
| `organization_id` | integer | yes |  |
| `organization_name` | string |  | The Organization's name, for someone who isn't in it yet. |
| `email` | string | yes | Who may accept it. Stored lowercased. |
| `role` | `"owner"` \| `"member"` | yes | The role they'll have once they accept. |
| `created_at` | integer | yes |  |
| `expires_at` | integer | yes | When the link stops working, 7 days after it was created. |

<a id="schema-newinvitation"></a>

### NewInvitation

An Invitation just created, with its link. The link's secret is shown only here.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the Invitation in routes. Not a secret. |
| `organization_id` | integer | yes |  |
| `organization_name` | string |  | The Organization's name, for someone who isn't in it yet. |
| `email` | string | yes | Who may accept it. Stored lowercased. |
| `role` | `"owner"` \| `"member"` | yes | The role they'll have once they accept. |
| `created_at` | integer | yes |  |
| `expires_at` | integer | yes | When the link stops working, 7 days after it was created. |
| `secret` | string | yes | The secret the link carries. Send it to `POST /v1/invitations/accept`. |
| `link` | string | yes | The dashboard page that accepts it. Send this to the person you invited. |

<a id="schema-member"></a>

### Member

An Account that belongs to an Organization.

| Field | Type | Required | Description |
|---|---|---|---|
| `account_id` | integer | yes | Identifies the Member in routes. |
| `email` | string | yes |  |
| `role` | `"owner"` \| `"member"` | yes | Every Member manages everything the Organization owns. An Owner can also manage its Members and Invitations. |
| `joined_at` | integer | yes | When the Account became a Member. |

<a id="schema-namespace"></a>

### Namespace

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | [NamespaceName](#schema-namespacename) | yes | A Namespace's name, which is the root of every path signed for it. It never changes. Use lowercase letters, digits and hyphens, and start and end with a letter or digit. Some names, like `admin` and `default`, are reserved, and so is every name that starts with `trial-`, which only Trial Organizations get. |
| `organization_id` | integer | yes | The Organization that owns it, for good. |
| `platform_peering` | boolean | yes | Whether its tracks may travel between your Self-hosted relays and Platform relays, the relays Mux runs. |
| `created_at` | integer | yes |  |

<a id="schema-namespacesettings"></a>

### NamespaceSettings

| Field | Type | Required | Description |
|---|---|---|---|
| `platform_peering` | boolean |  | Whether the Namespace's tracks may travel between your Self-hosted relays and Platform relays, the relays Mux runs. A change takes effect within about a minute. Your Regions can't link to Mux's yet, so for now this changes nothing. |

<a id="schema-accountenvelope"></a>

### AccountEnvelope

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | [Account](#schema-account) | yes |  |

<a id="schema-apikey"></a>

### APIKey

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the key for revoking it. Not a secret. |
| `label` | string | yes |  |
| `preview` | string | yes | The first 6 characters of the secret after `sb_`, so you can tell keys apart. |
| `created_at` | integer | yes |  |
| `expires_at` | integer |  | Absent for a key that never expires. |
| `last_used_at` | integer |  | Absent for a key that has never been used. |
| `namespaces` | array of [NamespaceName](#schema-namespacename) |  | The Namespaces the key is limited to, by name. Absent for a key with no limit. A deleted Namespace drops out, so a key whose Namespaces have all been deleted has `[]` here and can do nothing. |

<a id="schema-newapikey"></a>

### NewAPIKey

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the key for revoking it. Not a secret. |
| `label` | string | yes |  |
| `preview` | string | yes | The first 6 characters of the secret after `sb_`, so you can tell keys apart. |
| `created_at` | integer | yes |  |
| `expires_at` | integer |  | Absent for a key that never expires. |
| `last_used_at` | integer |  | Absent for a key that has never been used. |
| `namespaces` | array of [NamespaceName](#schema-namespacename) |  | The Namespaces the key is limited to, by name. Absent for a key with no limit. A deleted Namespace drops out, so a key whose Namespaces have all been deleted has `[]` here and can do nothing. |
| `secret` | string | yes | The API key itself. Shown only in this response. |

<a id="schema-scope"></a>

### Scope

What a Signing key may sign. `put` is publish and `get` is subscribe,
as in the token claims. Both are relative to `root`.

| Field | Type | Required | Description |
|---|---|---|---|
| `root` | string |  |  |
| `put` | array of string |  |  |
| `get` | array of string |  |  |

<a id="schema-jwk"></a>

### JWK

A Signing key in JSON Web Key form, with the `scope` extension moq
relays understand. `k` is the secret. Anyone holding it can sign
tokens within the scope.

| Field | Type | Required | Description |
|---|---|---|---|
| `alg` | `"HS256"` | yes |  |
| `key_ops` | array of string | yes |  |
| `kty` | `"oct"` | yes |  |
| `k` | string | yes | The HMAC secret, base64url without padding. |
| `kid` | string | yes |  |
| `scope` | [Scope](#schema-scope) | yes | What a Signing key may sign. `put` is publish and `get` is subscribe, as in the token claims. Both are relative to `root`. |

<a id="schema-signingkeysummary"></a>

### SigningKeySummary

| Field | Type | Required | Description |
|---|---|---|---|
| `kid` | string | yes |  |
| `alg` | `"HS256"` | yes |  |
| `scope` | [Scope](#schema-scope) | yes | What a Signing key may sign. `put` is publish and `get` is subscribe, as in the token claims. Both are relative to `root`. |
| `namespace` | string | yes |  |
| `created_at` | integer | yes |  |

<a id="schema-claims"></a>

### Claims

A Client token's payload. `root` is absolute and starts with your
Namespace. `put` (publish) and `get` (subscribe) are prefixes relative
to `root`.

| Field | Type | Required | Description |
|---|---|---|---|
| `root` | string |  |  |
| `put` | array of string |  |  |
| `get` | array of string |  |  |
| `exp` | integer |  | When the token expires. Absent for a token that never expires. |
| `iat` | integer |  | When the token was issued. |

<a id="schema-mintedtoken"></a>

### MintedToken

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | yes | A JWT. Give it to the Client, which passes it to the relay as `?jwt=`. |
| `kid` | string | yes |  |
| `claims` | [Claims](#schema-claims) | yes | A Client token's payload. `root` is absolute and starts with your Namespace. `put` (publish) and `get` (subscribe) are prefixes relative to `root`. |

<a id="schema-verifiedtoken"></a>

### VerifiedToken

| Field | Type | Required | Description |
|---|---|---|---|
| `kid` | string | yes |  |
| `claims` | [Claims](#schema-claims) | yes | A Client token's payload. `root` is absolute and starts with your Namespace. `put` (publish) and `get` (subscribe) are prefixes relative to `root`. |
| `expires_in` | integer |  | Seconds left. Absent for a token that never expires. |

<a id="schema-newenrollment"></a>

### NewEnrollment

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the enrollment for revoking it. Not the token. |
| `account` | string \| null | yes | The Organization's oldest Namespace, kept from before Organizations. The Node belongs to the whole Organization and carries all of its Namespaces. `null` when the Organization has no Namespaces. |
| `url` | string | yes | The URL as you sent it. |
| `canonical` | string | yes | The URL in the normalized form Switchboard uses to tell Nodes apart. |
| `region` | string | yes |  |
| `gateway` | boolean | yes |  |
| `client_url` | string |  | The Client URL the Node will have. Omitted when it has none. |
| `preview` | string | yes | The first 6 characters of the token after `sbe_`. |
| `created_at` | integer | yes |  |
| `expires_at` | integer | yes |  |
| `token` | string | yes | The Relay enrollment token. Shown only in this response. |

<a id="schema-enrollment"></a>

### Enrollment

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Identifies the enrollment for revoking it. Not the token. |
| `account` | string \| null | yes | The Organization's oldest Namespace, kept from before Organizations. The Node belongs to the whole Organization and carries all of its Namespaces. `null` when the Organization has no Namespaces. |
| `url` | string | yes | The URL as you sent it. |
| `canonical` | string | yes | The URL in the normalized form Switchboard uses to tell Nodes apart. |
| `region` | string | yes |  |
| `gateway` | boolean | yes |  |
| `client_url` | string |  | The Client URL the Node will have. Omitted when it has none. |
| `preview` | string | yes | The first 6 characters of the token after `sbe_`. |
| `created_at` | integer | yes |  |
| `expires_at` | integer | yes |  |

<a id="schema-node"></a>

### Node

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | The URL the Node's peers dial, as it was enrolled. |
| `canonical` | string | yes | The URL in the normalized form Switchboard uses to tell Nodes apart. |
| `source` | `"poll"` \| `"static"` | yes |  |
| `account` | string \| null | yes | The Organization's oldest Namespace, kept from before Organizations. The Node belongs to the whole Organization and carries all of its Namespaces. `null` when the Organization has no Namespaces. |
| `region` | string | yes |  |
| `gateway` | boolean | yes |  |
| `client_url` | string |  | The Client URL, which is where Clients, such as a player page, reach this relay when that isn't `url`. On a private network, `url` is often a name no Client can resolve, so whatever hands relay addresses to Clients reads this instead. It is fixed when the Node is enrolled, and omitted when the Node has none. Relays never see it, because peer discovery hands out only `url`. |
| `enrolled` | boolean | yes | Whether the Node came from a Relay enrollment token, so its relay has a Node key. |
| `first_seen_at` | integer | yes |  |
| `last_seen_at` | integer | yes | When its relay last polled, or when it enrolled if it hasn't polled since. |
| `live` | boolean | yes | Whether its relay has polled in the last 90 seconds. |

<a id="schema-protectedresource"></a>

### ProtectedResource

| Field | Type | Required | Description |
|---|---|---|---|
| `resource` | string (uri) | yes | This server's public origin, the one resource its Agent tokens may name. |
| `authorization_servers` | array of string (uri) | yes | The WorkOS AuthKit domain an Agent registers with. |

<a id="schema-health"></a>

### Health

| Field | Type | Required | Description |
|---|---|---|---|
| `status` | `"ok"` \| `"unavailable"` | yes |  |
