# Switchboard Relay API

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

Base URL: `https://switchboard.mux.global`

The endpoints a moq-relay calls. The only one you call yourself is
`POST /cluster/enroll`, once, when you set up a Self-hosted relay. The
rest are documented so you can see what your relay is doing and make
sense of its errors.

A relay proves who it is with a credential in the URL, as `jwt=`. A
Self-hosted relay uses its Node key (`sbn_...`). A request without one is
a 401.

These endpoints work with stock `moqdev/moq-relay` 0.15 or later. The
request and response shapes of `/cluster/connect` and `/auth` are
moq-relay's, not ours.

## Authentication

- **nodeKey** (query `jwt`): The Node key (`sbn_...`) from `POST /cluster/enroll`.

## Enrollment

Turning a Relay enrollment token into a Node key, once per relay.

### Exchange a Relay enrollment token for a Node key

```http
POST /cluster/enroll
```

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

Spends a Relay enrollment token from
`POST /v1/organizations/{id}/enrollments` and returns the Node, its
Node key, and the values to put in the relay's `relay.toml`. Run it
once per relay, by hand or from your provisioning script.

The token is the only credential, and it works once. A second exchange
is a 401. Keep the Node key. A relay that loses it has to be enrolled
again as a new Node.

**Request body**

Body `application/json`: object

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

**Responses**

- **201**: Enrolled.
  - `application/json`: object
- **400**: The request is malformed. The message says how.
  - `application/json`: [Error](#schema-error)
- **401**: The token is unknown, expired, or already used.
  - `application/json`: [Error](#schema-error)
- **409**: Another Node took this token's URL after the token was issued. The token still works once that Node is deleted.
  - `application/json`: [Error](#schema-error)

## Peer discovery

How a relay finds the other relays it should connect to.

### Get the relay's peers

```http
GET /cluster/connect
```

operationId: `connect` · [reference](https://www.caprid.com/docs/reference/http/relay/operations/connect/)

The relay polls this about once a second, using the `connect_api` URL
from enrollment, and dials every URL in the list. Each pair of peers is
dialed from one side only, so two relays in the same Region don't both
appear in each other's lists.

A relay that stops polling is dropped from its peers' lists after 90
seconds.

If Switchboard can't be reached, the relay keeps the peers it has.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `jwt` | query | string | yes | The Node key. |
| `node` | query | string |  | Added by the relay from `[cluster] node`. It has to match the URL the Node was enrolled with. |
| `region` | query | string |  | If present, it has to match the Region the Node was enrolled in. A Node's Region can't change. To move a relay, enroll it again. |
| `client` | query | string |  | If present, it has to match the `client_url` the Node was enrolled with, character for character. The relay can repeat its Client URL but can't set or change it. The poll is a 400 if the Node was enrolled without a `client_url`, or if the value is empty or doesn't start with `http://` or `https://`. A 400 here repeats on every poll, so a relay with a wrong `client=` gets no peers and, after 90 seconds, drops out of its peers' lists too. Leave it out unless you want the poll to check it. |
| `If-None-Match` | header | string |  |  |

**Responses**

- **200**: The URLs this relay should dial.
  - `application/json`: array of string (uri)
- **304**: The list hasn't changed since the ETag sent in If-None-Match.
- **400**: `node`, `region` or `client` doesn't match what the Node was enrolled with.
  - `application/json`: [Error](#schema-error)
- **401**: No credential, or the Node key is unknown. An unknown Node key usually means the Node was deleted, and the relay has to be enrolled again.
  - `application/json`: [Error](#schema-error)

## Admission

How a relay decides whether a Client may publish or subscribe.

### Decide whether a session may start

```http
POST /auth
```

operationId: `admit` · [reference](https://www.caprid.com/docs/reference/http/relay/operations/admit/)

The relay asks this for every session: once when a Client (or a peer
relay) connects, again every 30 seconds while it stays connected, and
once when it ends. It uses the `[auth] url` from enrollment.

Switchboard reads the Client's credential from the `query` in the
request body, usually a Client token as `jwt=`, and answers with a
Grant: what the session may publish and subscribe to, and when it
lapses.

Every Grant a Self-hosted relay gets is limited to its Organization's
Namespaces, whatever the Client's credential says.

Switchboard answers from the location nearest the relay, and keeps
answering while the rest of Switchboard is down. If the relay can't
reach it at all, sessions already running continue until their
Grant's `expires`, which is at most 5 minutes away. New sessions are
refused.

**Parameters**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `jwt` | query | string | yes | The relay's own Node key, not the Client's token. |

**Request body**

Body `application/json`: object

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string |  | The session's ID. |
| `event` | `"connect"` \| `"revalidate"` \| `"end"` | yes |  |
| `transport` | string |  |  |
| `path` | string |  | The path the Client dialed. |
| `query` | string |  | The Client's raw query string. Its last `jwt=` is the credential. |
| `tls` | object |  | Present when the Client presented a certificate the relay verified. |

**Responses**

- **200**: Admitted, with this Grant.
  - `application/json`: [Grant](#schema-grant)
- **204**: Answer to an `end` event.
- **400**: The request is malformed. The message says how.
  - `application/json`: [Error](#schema-error)
- **401**: The relay's own credential is missing or unknown.
  - `application/json`: [Error](#schema-error)
- **403**: The session is refused. The message says why, and the relay writes it to its log. The Client sees a 401.
  - `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/relay/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-node"></a>

### Node

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes |  |
| `canonical` | string | yes |  |
| `source` | `"poll"` \| `"static"` | yes |  |
| `account` | string | yes | The oldest Namespace of the Organization that owns the Node. It names the owner. The relay still carries all of the Organization's Namespaces. |
| `region` | string | yes |  |
| `gateway` | boolean | yes |  |
| `client_url` | string |  | The address Clients, such as a player page, use to reach this relay, when it differs from `url`, the address its peers dial. On a private network, `url` can be a name no Client can resolve, so whatever hands relay addresses to your Clients should read this field instead. It's fixed at enrollment and omitted when the Node has none. `/cluster/connect` never returns it, because that list is only for peers to dial. |
| `enrolled` | boolean | yes |  |
| `first_seen_at` | integer | yes |  |
| `last_seen_at` | integer | yes |  |
| `live` | boolean | yes |  |

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

### Grant

moq-relay's Grant. `publish` and `subscribe` are patterns relative to
the dialed path: `**` is everything, `live/**` is `live` and everything
under it.

| Field | Type | Required | Description |
|---|---|---|---|
| `publish` | array of string |  |  |
| `subscribe` | array of string |  |  |
| `expires` | integer |  | When the session ends if no revalidation succeeds before then. At most 5 minutes out, and never later than the Client token's own expiry. |
| `revalidate` | integer |  | Seconds until the relay asks again. Currently 30. |
| `tier` | string |  | The stats tier the relay counts this session's traffic under. It appears only when the session is another relay (`peer` is true) and Switchboard is configured with a peer tier. When it's absent, the relay counts the session in the default tier, with Client traffic. |
| `peer` | boolean |  | The session is another relay, not a Client. |

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

### Health

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