{
  "openapi": "3.1.0",
  "info": {
    "title": "Switchboard Account API",
    "version": "1",
    "description": "Manage your Organizations on Switchboard: their Namespaces, the Signing\nkeys and Client tokens for each Namespace, their API keys, and the\nSelf-hosted relays they enroll.\n\nAn Organization is a customer, and a Namespace is a path root it owns,\nlike an S3 bucket. Routes name what they act on. Everything that serves\nthe whole Organization (API keys, Nodes, enrollments) is under\n`/v1/organizations/{id}`. Everything inside one Namespace (Signing keys,\nClient tokens, its settings) is under `/v1/namespaces/{namespace}`. That\npath needs no Organization, because Namespace names are unique.\n\nEvery request except signup, login, logout and the health check needs a\ncredential. Send an API key as `Authorization: Bearer sb_...`. An API key\nbelongs to one Organization and reaches only that Organization and its\nNamespaces. You can limit a key to some of those Namespaces, so a staging\nCI key can't read prod's Signing keys. A limited key gets a 403 outside\nits Namespaces and on every `/v1/organizations/{id}` route. The dashboard\nuses a session cookie instead, which reaches every Organization the\nsigned-in person is a Member of. Anything you can't reach is a 404,\nwhether or not it exists.\n\nOn a server that logs people in through WorkOS, such as\napp.caprid.com, there are no passwords. You sign in at `/app/` in\na browser, and create API keys there for your scripts. On that server,\n`POST /v1/login` returns 410, and `POST /v1/signup` works only as the\nlast step of signing in through the browser.\n\nAn AI agent can use the API without you creating an API key for it, on\na server set up for Agents. The Agent registers with WorkOS AuthKit, by\nfollowing the steps at `/auth.md`, and sends the access token AuthKit\ngives it, its Agent token, as `Authorization: Bearer ...`. Once you claim the Agent at AuthKit, it acts\nas your Account, so it can do anything your dashboard session can,\nincluding creating API keys for itself. If you haven't signed up yet, the\nAgent gets a 403 on every route until you do.\n\nAn Agent nobody has claimed can start right away. Its first request gives\nit a Trial Organization with one Namespace, named `trial-` and a random\ntag, so it can create Signing keys, mint Client tokens for Platform\nrelays, and create API keys for itself, with no person involved. A trial\nhas no Members. It can't enroll Self-hosted relays, create or delete a\nNamespace, create an Organization, or manage Members and Invitations, and\nthose routes return 403 to the Agent and to the trial's API keys alike.\nStarting trials is rate-limited per address, like signup, so an Agent's\nfirst request can return 429.\n\nThe trial lapses 7 days after it starts, at its `trial_expires_at`. From\nthen on its API keys get a 401, and the Agent gets a 403 that says the\ntrial is over. Within about 10 minutes Switchboard deletes the trial's\nNamespace and its Signing keys, so relays stop admitting its Client\ntokens, and nobody can take the name again.\n\nClaim the Agent before then to keep what it made. Its first request after\nyou claim it makes you the trial's Owner, and the Organization stops\nbeing a trial. It no longer lapses, its limits lift, and its Namespace,\nSigning keys and API keys keep working. It doesn't count against the\nOrganizations you may create. If you claim the Agent before you sign up,\nthe trial waits for you until it lapses. A trial that lapsed before the\nclaim stays lapsed.\n\nPaths you send (`path`, `publish`, `subscribe`) are relative to the\nNamespace, so `path: \"room/42\"` sent to `/v1/namespaces/acme/tokens` is\n`acme/room/42`.\n\nThe routes from before Organizations (`/v1/keys`, `/v1/tokens`,\n`/v1/enrollments`, `/v1/nodes`, `/v1/account/api-keys`, and `PATCH\n/v1/account`) still work with an API key, acting in its Organization.\n`/v1/keys`, `/v1/tokens` and `PATCH /v1/account` work only while the key\nreaches exactly one Namespace: its Organization's only one, or the only\none in its limit. The others act on the whole Organization, so a limited\nkey gets a 403. With a session, they return 400 and name the route to use.\n\nRequest bodies are JSON. An empty body is the same as `{}`, and unknown\nfields are ignored. Times are Unix seconds. Errors are always\n`{\"error\": \"...\"}`, with a message meant for a person to read.\n",
    "contact": {
      "name": "Mux",
      "url": "https://www.mux.com"
    }
  },
  "servers": [
    {
      "url": "https://app.caprid.com"
    }
  ],
  "security": [
    {
      "apiKey": []
    },
    {
      "session": []
    },
    {
      "agentToken": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Signing up, logging in, and who you are."
    },
    {
      "name": "Organizations",
      "description": "The customers you manage, and their names."
    },
    {
      "name": "Members",
      "description": "Who belongs to an Organization. Owners manage Members, signed in with a\nsession. Any Member can leave.\n"
    },
    {
      "name": "Invitations",
      "description": "How people join an Organization. An Owner invites an email address and\nsends the link themselves. Only the person with that email can accept.\n"
    },
    {
      "name": "Namespaces",
      "description": "The path roots an Organization owns, and their settings."
    },
    {
      "name": "API keys",
      "description": "`sb_...` keys that authenticate calls to this API. A relay never\naccepts one. A key can be limited to some of its Organization's\nNamespaces when it is created.\n"
    },
    {
      "name": "Signing keys",
      "description": "Organization signing keys, which sign Client tokens. You can hold the secret\nand sign Client tokens in your own backend without calling Switchboard.\n"
    },
    {
      "name": "Client tokens",
      "description": "Tokens a Client presents to a relay to publish or subscribe. Minting one\nhere is a convenience. Anyone holding the Signing key can mint the same\ntoken offline.\n"
    },
    {
      "name": "Self-hosted relays",
      "description": "Enrolling relays you run yourself into your Organization, and removing\nthem. A Self-hosted relay carries all of its Organization's Namespaces.\n"
    },
    {
      "name": "Before Organizations",
      "description": "The routes from before Organizations, kept for API keys. Each one does\nwhat a newer route does, in the API key's own Organization.\n"
    },
    {
      "name": "Agents",
      "description": "How an AI agent that finds this server learns where to register, on a\nserver set up for Agents. Neither route needs a credential.\n"
    },
    {
      "name": "Health",
      "description": "Liveness, for load balancers and monitoring."
    }
  ],
  "paths": {
    "/v1/signup": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getPendingSignup",
        "summary": "Show a signup in progress",
        "description": "Only with WorkOS login. After a first sign-in, the person has proved\nwho they are but has no Account yet. This route returns the email\nWorkOS verified, so the dashboard can ask for a Namespace. It also\nlists the pending Invitations to that email, so the dashboard can\noffer to join one of those Organizations instead. The login callback\nsets a cookie that holds the signup in progress for 15 minutes.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The signup in progress.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "email",
                    "invitations"
                  ],
                  "properties": {
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "invitations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invitation"
                      }
                    }
                  }
                },
                "example": {
                  "email": "alice@acme.example",
                  "invitations": [
                    {
                      "id": "3f9c1a2b7d4e",
                      "organization_id": 4,
                      "organization_name": "Acme",
                      "email": "alice@acme.example",
                      "role": "member",
                      "created_at": 1790294400,
                      "expires_at": 1790899200
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No signup is in progress, or this server doesn't use WorkOS login.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Account"
        ],
        "operationId": "signup",
        "summary": "Create an Account",
        "description": "Creates an Account. Every Account starts by doing one of two things:\n\n- **Creating an Organization.** Send `namespace`. The Organization is\n  named after the Namespace (you can rename it later), you are its\n  Owner, and the response carries its first API key. This is the\n  only time that key's secret is shown, so store it now.\n- **Joining one.** Send `invitation` instead, and you become a Member\n  of the Organization that invited you, with the role it invited you\n  as. You get no API key, because the Organization already has its\n  own. Without WorkOS login, send the secret from the Invitation's\n  link, because nothing has verified your email. With WorkOS login,\n  send the Invitation's `id` from `GET /v1/signup`.\n\nThe response also sets a session cookie, for a signup that came from\nthe dashboard.\n\nPassword signups are limited to 20 an hour from each client address.\n\nWith WorkOS login, the body needs only `namespace` or `invitation`.\nThe email is the one WorkOS verified, there is no password, and the\nrequest has to carry the cookie the login callback set.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send `namespace` or `invitation`. `email` and `password` are\nrequired too, unless the server uses WorkOS login.\n",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254,
                    "description": "Stored lowercased. `Alice@example.com` and `alice@example.com` are the same Account."
                  },
                  "password": {
                    "type": "string",
                    "minLength": 12,
                    "maxLength": 1024,
                    "description": "Length is the only rule. Counted in characters, not bytes."
                  },
                  "namespace": {
                    "$ref": "#/components/schemas/NamespaceName"
                  },
                  "invitation": {
                    "type": "string",
                    "description": "Join the Organization this Invitation is for, instead of\ncreating one. Send the secret from its link, or with\nWorkOS login, its `id`. Your email has to be the one\ninvited.\n"
                  }
                }
              },
              "examples": {
                "create": {
                  "summary": "Create an Organization",
                  "value": {
                    "email": "dev@acme.example",
                    "password": "correct horse battery staple",
                    "namespace": "acme"
                  }
                },
                "join": {
                  "summary": "Join with an Invitation's link",
                  "value": {
                    "email": "alice@acme.example",
                    "password": "correct horse battery staple",
                    "invitation": "sbi_mR3x9Qe2vT8wY1zA4bC7dF0gH5jK6lN2pS8uV3xZ1qW"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The Account, and when it created an Organization, that Organization's first API key with its secret.",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account"
                  ],
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    },
                    "api_key": {
                      "$ref": "#/components/schemas/NewAPIKey"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "With WorkOS login, there is no signup in progress. Sign in at `/app/` first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The Invitation is for another email address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such Invitation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The email or the Namespace is already taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "$ref": "#/components/responses/InvitationSpent"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/login": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "startLogin",
        "summary": "Sign in with WorkOS",
        "description": "Only with WorkOS login. Open this in a browser. It redirects to the\nWorkOS sign-in page, which sends the browser back to\n`/v1/login/callback`. The dashboard's sign-in button links here.\n",
        "security": [],
        "responses": {
          "302": {
            "description": "Redirect to WorkOS."
          },
          "404": {
            "$ref": "#/components/responses/NoWorkOS"
          }
        }
      },
      "post": {
        "tags": [
          "Account"
        ],
        "operationId": "login",
        "summary": "Log in",
        "description": "Exchanges an email and password for a session cookie. Sessions last 14\ndays. Scripts should use an API key instead.\n\nFailed logins are limited to 10 per 15 minutes, counted separately per\nemail and per client address.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged in.",
            "headers": {
              "Set-Cookie": {
                "$ref": "#/components/headers/SessionCookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The email and password don't match an Account. The response is the same whether or not the email exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "This server logs people in through WorkOS, and passwords no longer work.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/login/callback": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "finishLogin",
        "summary": "Finish signing in with WorkOS",
        "description": "WorkOS sends the browser back here. You never call it yourself.\n\nA returning person gets a session cookie. A first sign-in whose\nverified email matches an existing Account is linked to it, once and\nfor good. Anyone else gets a signup in progress, and the dashboard asks\nthem for a Namespace.\n\nEither way the browser lands on `/app/`. When signing in didn't work,\n`?login=` says why: `failed`, `unverified` (WorkOS hasn't verified the\nemail), or `conflict` (the email's Account is linked to a different\nWorkOS user).\n",
        "security": [],
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the dashboard."
          },
          "404": {
            "$ref": "#/components/responses/NoWorkOS"
          }
        }
      }
    },
    "/v1/logout": {
      "post": {
        "tags": [
          "Account"
        ],
        "operationId": "logout",
        "summary": "Log out",
        "description": "Ends the calling session and clears its cookie. Works with an expired\ncookie, or none.\n\nA session that came from WorkOS also has a WorkOS session to end, which\nonly the browser can do. The response says where to send it.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Logged out here. Send the browser to `logout_url` to end the WorkOS session too.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "logout_url"
                  ],
                  "properties": {
                    "logout_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "204": {
            "description": "Logged out."
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getAccount",
        "summary": "Get the caller",
        "description": "Who you are and which Organizations you can manage. With a session,\nthat's every Organization you're a Member of. An Agent you claimed\ngets the same answer as your session. With an API key, it's\nthe key's own Organization, and there's no email, because an API key\nbelongs to the Organization rather than to a person. Instead,\n`api_key` describes the key itself, with the Namespaces it is limited\nto.\n",
        "responses": {
          "200": {
            "description": "The caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountEnvelope"
                },
                "examples": {
                  "session": {
                    "summary": "A session that belongs to two Organizations",
                    "value": {
                      "account": {
                        "email": "alice@acme.example",
                        "created_at": 1760000000,
                        "organizations": [
                          {
                            "id": 4,
                            "name": "Acme",
                            "role": "owner",
                            "created_at": 1760000000
                          },
                          {
                            "id": 9,
                            "name": "Globex",
                            "role": "member",
                            "created_at": 1760500000
                          }
                        ]
                      }
                    }
                  },
                  "api-key": {
                    "summary": "An API key whose Organization has one Namespace",
                    "value": {
                      "account": {
                        "api_key": {
                          "id": "3f9a1c07be42",
                          "label": "initial",
                          "preview": "R2x0Z9",
                          "created_at": 1760000000,
                          "last_used_at": 1760600000
                        },
                        "organizations": [
                          {
                            "id": 4,
                            "name": "Acme",
                            "created_at": 1760000000
                          }
                        ],
                        "namespace": "acme",
                        "platform_peering": false
                      }
                    }
                  },
                  "limited-api-key": {
                    "summary": "An API key limited to two Namespaces",
                    "value": {
                      "account": {
                        "api_key": {
                          "id": "8c21d4e09a7f",
                          "label": "deploys",
                          "preview": "Qm9vTx",
                          "created_at": 1760700000,
                          "last_used_at": 1760800000,
                          "namespaces": [
                            "acme-prod",
                            "acme-staging"
                          ]
                        },
                        "organizations": [
                          {
                            "id": 4,
                            "name": "Acme",
                            "created_at": 1760000000
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AgentRefused"
          }
        }
      },
      "patch": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "updateAccountLegacy",
        "summary": "Change the Namespace's settings",
        "deprecated": true,
        "description": "`PATCH /v1/namespaces/{namespace}` for the API key's only Namespace, answered with the caller.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NamespaceSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The caller, with the change applied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/organizations": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "operationId": "listOrganizations",
        "summary": "List your Organizations",
        "description": "Every Organization your session is a Member of, with your role in\neach, oldest membership first. An API key lists only its own\nOrganization, with no role.\n",
        "responses": {
          "200": {
            "description": "The Organizations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Organization"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/AgentRefused"
          }
        }
      },
      "post": {
        "tags": [
          "Organizations"
        ],
        "operationId": "createOrganization",
        "summary": "Create an Organization",
        "description": "Creates another Organization, the way signup created your first. Use\none to keep a separate customer, or a project you'll hand over, apart\nfrom the rest. It has its own Namespaces, API keys, Members and\nrelays.\n\nYou become its Owner. It is named after its Namespace unless you give\nit a `name`, which you can change later. The response holds its first\nAPI key, and this is the only time that key's secret is shown, so\nstore it now.\n\nThis takes a signed-in session. An API key acts for its own\nOrganization, so it can't create another.\n\nAn Account may create 5 Organizations. Organizations you joined by\ninvitation don't count. If you need more, ask Mux to raise your cap.\n",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "namespace"
                ],
                "properties": {
                  "namespace": {
                    "$ref": "#/components/schemas/NamespaceName"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "For display. Defaults to the Namespace."
                  }
                }
              },
              "example": {
                "namespace": "acme-labs",
                "name": "Acme Labs"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new Organization, its Namespace, and its first API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "organization",
                    "namespace",
                    "api_key"
                  ],
                  "properties": {
                    "organization": {
                      "$ref": "#/components/schemas/Organization"
                    },
                    "namespace": {
                      "$ref": "#/components/schemas/Namespace"
                    },
                    "api_key": {
                      "$ref": "#/components/schemas/NewAPIKey"
                    }
                  }
                },
                "example": {
                  "organization": {
                    "id": 12,
                    "name": "Acme Labs",
                    "role": "owner",
                    "created_at": 1792900000
                  },
                  "namespace": {
                    "name": "acme-labs",
                    "organization_id": 12,
                    "platform_peering": false,
                    "created_at": 1792900000
                  },
                  "api_key": {
                    "id": "3f9a1c07be42",
                    "label": "initial",
                    "preview": "R2x0Z9",
                    "created_at": 1792900000,
                    "secret": "sb_R2x0Z9kQ7mVt3xLp8wNc1bYh5sDf2gJr6uEa0zWiTq4"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Sent with an API key, or you have already created as many\nOrganizations as you may. The message says which.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "an Account may create 5 Organizations, and you have already; ask whoever runs this Switchboard to raise your cap"
                }
              }
            }
          },
          "409": {
            "description": "The Namespace is taken. Namespace names are never reused, even after one is deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/organizations/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Organizations"
        ],
        "operationId": "getOrganization",
        "summary": "Get an Organization",
        "responses": {
          "200": {
            "description": "The Organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Organizations"
        ],
        "operationId": "updateOrganization",
        "summary": "Rename an Organization",
        "description": "The name is only for display, so any Member can change it, and so\ncan the Organization's API keys. Fields you leave out are left alone.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100,
                    "description": "Leading and trailing spaces are trimmed."
                  }
                }
              },
              "example": {
                "name": "Acme, Inc."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Organization, renamed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/namespaces": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Namespaces"
        ],
        "operationId": "listNamespaces",
        "summary": "List an Organization's Namespaces",
        "description": "Oldest first.",
        "responses": {
          "200": {
            "description": "The Namespaces.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Namespace"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Namespaces"
        ],
        "operationId": "createNamespace",
        "summary": "Create a Namespace",
        "description": "Gives the Organization another Namespace, such as `acme-staging`\nbeside `acme`, so that staging's Signing keys and Client tokens can't\nreach prod. Any Member can create one, and so can an API key that\nisn't limited to some Namespaces.\n\nNames follow the same rules as at signup, and are unique across\nSwitchboard for good. A name that any Namespace has ever had,\nincluding a deleted one, is never issued again.\n\nAn Organization can have 10 live Namespaces. Deleted ones don't\ncount. If you need more, ask Mux to raise the cap.\n\nA new Namespace starts with `platform_peering` off.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "namespace"
                ],
                "properties": {
                  "namespace": {
                    "$ref": "#/components/schemas/NamespaceName"
                  }
                }
              },
              "example": {
                "namespace": "acme-staging"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new Namespace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Namespace"
                },
                "example": {
                  "name": "acme-staging",
                  "organization_id": 7,
                  "platform_peering": false,
                  "created_at": 1790294400
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "The Organization already has as many Namespaces as it may, or you\nsent an API key limited to some Namespaces. The message says which.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "an Organization may have 10 Namespaces, and this one has; delete one, or ask whoever runs this Switchboard to raise the cap"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The name is taken, or was once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "the name acme-staging is taken, or was once: a Namespace's name is never reissued"
                }
              }
            }
          }
        }
      }
    },
    "/v1/organizations/{id}/members": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Members"
        ],
        "operationId": "listMembers",
        "summary": "List an Organization's Members",
        "description": "Everyone who belongs to the Organization, with their role and when\nthey joined, longest-standing first. Any Member can list them, and so\ncan the Organization's API keys.\n",
        "responses": {
          "200": {
            "description": "The Members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Member"
                  }
                },
                "example": [
                  {
                    "account_id": 7,
                    "email": "bob@acme.example",
                    "role": "owner",
                    "joined_at": 1790294400
                  },
                  {
                    "account_id": 12,
                    "email": "alice@acme.example",
                    "role": "member",
                    "joined_at": 1790380800
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/members/{account}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        },
        {
          "name": "account",
          "in": "path",
          "required": true,
          "description": "The Member's `account_id`, from the list.",
          "schema": {
            "type": "integer"
          },
          "example": 12
        }
      ],
      "patch": {
        "tags": [
          "Members"
        ],
        "operationId": "updateMember",
        "summary": "Change a Member's role",
        "description": "Owners only, signed in with a session. An API key can never manage\nMembers, so a leaked one can't be used to let its holder in.\n\nAn Organization always has an Owner. To step down as the last one,\nmake someone else an Owner first.\n",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member"
                    ]
                  }
                }
              },
              "example": {
                "role": "owner"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Member, with the new role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "You aren't an Owner, or you sent an API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "only an Owner can manage this Organization's Members"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "This would leave the Organization without an Owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "an Organization must always have an Owner; make someone else an Owner first"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Members"
        ],
        "operationId": "deleteMember",
        "summary": "Remove a Member, or leave",
        "description": "Owners can remove anyone, and any Member can remove themselves, which\nis how you leave an Organization. Either way, it takes a session. An\nAPI key can never manage Members.\n\nThe last Owner can't leave or be removed. Make someone else an Owner\nfirst.\n\nAPI keys the Member created keep working. They belong to the\nOrganization, so CI doesn't break when someone leaves. Revoke them\nseparately if you need to.\n",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "204": {
            "description": "Removed. The Account no longer sees the Organization."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "You aren't an Owner and this isn't you, or you sent an API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "only a session can manage Members, never an API key"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "You are removing the last Owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "an Organization must always have an Owner; make someone else an Owner first"
                }
              }
            }
          }
        }
      }
    },
    "/v1/organizations/{id}/invitations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Invitations"
        ],
        "operationId": "listInvitations",
        "summary": "List an Organization's pending Invitations",
        "description": "Invitations that are still pending, oldest first. A pending\nInvitation hasn't been accepted, revoked or expired. Their links aren't listed, because a link's secret is shown\nonly once, when you create the Invitation.\n\nOwners only, signed in with a session.\n",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The pending Invitations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/NotAnOwner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Invitations"
        ],
        "operationId": "createInvitation",
        "summary": "Invite someone",
        "description": "Invites one email address into the Organization, and returns a link\nfor that person. Switchboard sends no email, so copy the link and send\nit yourself. Only the person with that email can use the link, so it's\nsafe if someone forwards it by mistake.\n\nThe link works once, and expires after 7 days. Its secret is shown\nonly in this response.\n\nOwners only, signed in with a session. An API key can never invite\nanyone, so a leaked one can't be used to let its holder in.\n",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Matched without regard to case."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member"
                    ],
                    "default": "member",
                    "description": "The role they'll have once they accept."
                  }
                }
              },
              "example": {
                "email": "alice@acme.example",
                "role": "member"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The Invitation, with its link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewInvitation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/NotAnOwner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "That email already belongs to a Member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "that email already belongs to a Member of this organization"
                }
              }
            }
          }
        }
      }
    },
    "/v1/organizations/{id}/invitations/{invitation}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        },
        {
          "name": "invitation",
          "in": "path",
          "required": true,
          "description": "The Invitation's `id`, from the list.",
          "schema": {
            "type": "string"
          },
          "example": "3f9c1a2b7d4e"
        }
      ],
      "delete": {
        "tags": [
          "Invitations"
        ],
        "operationId": "revokeInvitation",
        "summary": "Revoke an Invitation",
        "description": "Its link stops working. Owners only, signed in with a session.\n",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/NotAnOwner"
          },
          "404": {
            "description": "No such pending Invitation. One already accepted, revoked or expired is a 404 too.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invitations": {
      "get": {
        "tags": [
          "Invitations"
        ],
        "operationId": "listMyInvitations",
        "summary": "List the Invitations to your email",
        "description": "Only with WorkOS login, which has verified your email. Lists the\npending Invitations to it, from every Organization, so you can\naccept one without its link. The dashboard shows them when you sign\nup.\n",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The pending Invitations to your email.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Invitation"
                  }
                },
                "example": [
                  {
                    "id": "3f9c1a2b7d4e",
                    "organization_id": 4,
                    "organization_name": "Acme",
                    "email": "alice@acme.example",
                    "role": "member",
                    "created_at": 1790294400,
                    "expires_at": 1790899200
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "Sent with an API key, or with a session whose email WorkOS hasn't verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoWorkOS"
          }
        }
      }
    },
    "/v1/invitations/accept": {
      "post": {
        "tags": [
          "Invitations"
        ],
        "operationId": "acceptInvitation",
        "summary": "Accept an Invitation",
        "description": "Makes you a Member of the Organization, with the role the Invitation\nnames. Your email has to be the one invited, even if you have the\nlink. Each Invitation can be accepted once.\n\nSend `secret`, the last part of the link (`/app/invitations/{secret}`).\nWith WorkOS login, which has verified your email, you can send the\nInvitation's `id` instead. Without WorkOS, nothing has verified your\nemail, so only the link works.\n\nThis takes a session. An API key belongs to an Organization, not a\nperson.\n",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "secret": {
                    "type": "string",
                    "description": "The secret from the Invitation's link."
                  },
                  "id": {
                    "type": "string",
                    "description": "The Invitation's id. Only with WorkOS login."
                  }
                }
              },
              "example": {
                "secret": "sbi_mR3x9Qe2vT8wY1zA4bC7dF0gH5jK6lN2pS8uV3xZ1qW"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Organization you joined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                },
                "example": {
                  "id": 4,
                  "name": "Acme",
                  "role": "member",
                  "created_at": 1790294400
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "The Invitation is for another email address, you sent an `id`\non a server that hasn't verified your email, or you sent an API\nkey.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "this invitation is for another email address: sign in as the address it was sent to"
                }
              }
            }
          },
          "404": {
            "description": "No such Invitation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "You're already a Member of that Organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "$ref": "#/components/responses/InvitationSpent"
          }
        }
      }
    },
    "/v1/namespaces/{namespace}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Namespace"
        }
      ],
      "get": {
        "tags": [
          "Namespaces"
        ],
        "operationId": "getNamespace",
        "summary": "Get a Namespace",
        "responses": {
          "200": {
            "description": "The Namespace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Namespace"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Namespaces"
        ],
        "operationId": "updateNamespace",
        "summary": "Change a Namespace's settings",
        "description": "Fields you leave out are left alone.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NamespaceSettings"
              },
              "example": {
                "platform_peering": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Namespace, with the change applied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Namespace"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Namespaces"
        ],
        "operationId": "deleteNamespace",
        "summary": "Delete a Namespace",
        "description": "Deletes the Namespace and its Signing keys. Client tokens they signed\nstop working, and relays stop carrying the Namespace's tracks when\nthey next check a session, within about a minute.\n\nThis can't be undone, and the name can never be used again, by you\nor anyone else.\n\nOwners only, signed in with a session. An API key can never delete a\nNamespace, so a leaked one can't destroy something you can't get\nback.\n\nYour Nodes, Regions and enrollments belong to the Organization and\nstay as they are. You can delete the Organization's last Namespace.\nThe Organization stays, with no Namespaces, until you create another.\n",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "description": "You aren't an Owner, or you sent an API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "only an Owner can delete a Namespace"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/api-keys": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "API keys"
        ],
        "operationId": "listAPIKeys",
        "summary": "List API keys",
        "description": "Oldest first. Secrets are never listed, only a preview of each.",
        "responses": {
          "200": {
            "description": "The Organization's API keys, expired ones included.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/APIKey"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "API keys"
        ],
        "operationId": "createAPIKey",
        "summary": "Create an API key",
        "description": "The response is the only time the secret is shown. Switchboard keeps only a hash of it.",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateAPIKey"
        },
        "responses": {
          "201": {
            "description": "The new API key, with its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewAPIKey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/api-keys/{key}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "delete": {
        "tags": [
          "API keys"
        ],
        "operationId": "deleteAPIKey",
        "summary": "Revoke an API key",
        "description": "The key stops working on the next request that presents it.",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "The key's `id` from the list. Not its secret.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/namespaces/{namespace}/keys": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Namespace"
        }
      ],
      "get": {
        "tags": [
          "Signing keys"
        ],
        "operationId": "listSigningKeys",
        "summary": "List Signing keys",
        "description": "Oldest first, without their secrets. Get a key by its `kid` to see its secret.",
        "responses": {
          "200": {
            "description": "The Namespace's Signing keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SigningKeySummary"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Signing keys"
        ],
        "operationId": "createSigningKey",
        "summary": "Create a Signing key",
        "description": "Creates an Organization signing key in this Namespace and returns it\nas a JWK, secret included. Keep the secret in your backend to sign\nClient tokens there without calling Switchboard.\n\nThe key's scope is fixed when you create it and can't be widened\nlater. By default, a key covers your whole Namespace for both\npublishing and subscribing. Use `path` to root it lower down, and\n`publish` or `subscribe` to limit what the tokens it signs may grant.\nIf you send only one of `publish` and `subscribe`, the key can't sign\ntokens for the other role.\n\nRelays look keys up by `kid` through Switchboard, never from the Client,\nso a relay accepts a token signed with this key only within its scope.\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateSigningKey"
        },
        "responses": {
          "201": {
            "description": "The new key, secret included.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JWK"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The Namespace already has a key with this label.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/namespaces/{namespace}/keys/{kid}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Namespace"
        },
        {
          "name": "kid",
          "in": "path",
          "required": true,
          "description": "The key's `kid`, which is `<namespace>_<label>`.",
          "schema": {
            "type": "string",
            "example": "acme_backend"
          }
        }
      ],
      "get": {
        "tags": [
          "Signing keys"
        ],
        "operationId": "getSigningKey",
        "summary": "Get a Signing key",
        "description": "Returns the full JWK, secret included. A `kid` that isn't one of this Namespace's is a 404.",
        "responses": {
          "200": {
            "description": "The key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JWK"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Signing keys"
        ],
        "operationId": "deleteSigningKey",
        "summary": "Delete a Signing key",
        "description": "Every Client token the key signed stops working. Sessions already\nrunning on those tokens end within about a minute, the next time\ntheir relay checks them.\n",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/namespaces/{namespace}/tokens": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Namespace"
        }
      ],
      "post": {
        "tags": [
          "Client tokens"
        ],
        "operationId": "mintClientToken",
        "summary": "Mint a Client token",
        "description": "Signs a Client token with one of the Namespace's Signing keys. Use it\nwhen you'd rather not hold a Signing key's secret in your own backend.\n\nA token gets no grants by default. Name what it may publish, subscribe\nto, or both. A request that names neither is a 400. Whatever you ask\nfor has to fit inside the Signing key's scope.\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/MintClientToken"
        },
        "responses": {
          "200": {
            "description": "The signed token and what it grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedToken"
                }
              }
            }
          },
          "400": {
            "description": "The request can't be signed: it grants nothing, it asks for more\nthan the key's scope, it reaches outside the Namespace, or it names\nno key while the Namespace has zero or several.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The Namespace isn't one you can reach, or the `kid` isn't one of its keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/namespaces/{namespace}/tokens/verify": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Namespace"
        }
      ],
      "post": {
        "tags": [
          "Client tokens"
        ],
        "operationId": "verifyClientToken",
        "summary": "Check a Client token",
        "description": "Verifies a token against this Namespace's Signing keys and returns its\nclaims. Use it to debug a token a relay refuses. A token signed by any\nother Namespace's key gets a 401.\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/VerifyClientToken"
        },
        "responses": {
          "200": {
            "description": "The token is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifiedToken"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "The token is malformed, expired, has a bad signature, or wasn't\nsigned by one of this Namespace's keys. Also returned when the caller\nisn't authenticated.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/enrollments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Self-hosted relays"
        ],
        "operationId": "listEnrollments",
        "summary": "List pending enrollments",
        "description": "Relay enrollment tokens that haven't been used or expired yet. Oldest first.",
        "responses": {
          "200": {
            "description": "The pending enrollments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Enrollment"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Self-hosted relays"
        ],
        "operationId": "createEnrollment",
        "summary": "Create a Relay enrollment token",
        "description": "Issues a one-time token that a relay you run exchanges for its Node key,\nwith `POST /cluster/enroll`. You decide everything about the Node\nhere: its URL, its Region, whether it can be a Gateway, and its Client\nURL. The relay can't change any of it later.\n\nThe token is shown once. It expires after `expires_in` seconds, or\nwhen it's used, whichever comes first.\n",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateEnrollment"
        },
        "responses": {
          "201": {
            "description": "The pending enrollment, with its token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewEnrollment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "A Node already exists at this URL. Delete it first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/organizations/{id}/enrollments/{enrollment}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "delete": {
        "tags": [
          "Self-hosted relays"
        ],
        "operationId": "deleteEnrollment",
        "summary": "Revoke a pending enrollment",
        "description": "The token stops working. A Node that already enrolled with it stays enrolled.",
        "parameters": [
          {
            "name": "enrollment",
            "in": "path",
            "required": true,
            "description": "The enrollment's `id`, from the list. Not the token.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/organizations/{id}/nodes": {
      "parameters": [
        {
          "$ref": "#/components/parameters/OrganizationID"
        }
      ],
      "get": {
        "tags": [
          "Self-hosted relays"
        ],
        "operationId": "listNodes",
        "summary": "List your Nodes",
        "description": "Every Node the Organization has enrolled, running or not.",
        "responses": {
          "200": {
            "description": "The Nodes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Node"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Self-hosted relays"
        ],
        "operationId": "deleteNode",
        "summary": "Delete a Node",
        "description": "Deletes the Node and its Node key. Its relay is refused from its next\npoll, and its peers drop it. To bring the relay back, enroll it again\nwith a new Relay enrollment token.\n",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "The Node's URL, as listed.",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/account/api-keys": {
      "get": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "listAPIKeysLegacy",
        "summary": "List API keys",
        "deprecated": true,
        "description": "`GET /v1/organizations/{id}/api-keys` for the API key's Organization.",
        "responses": {
          "200": {
            "description": "The Organization's API keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/APIKey"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "createAPIKeyLegacy",
        "summary": "Create an API key",
        "deprecated": true,
        "description": "`POST /v1/organizations/{id}/api-keys` for the API key's Organization.",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateAPIKey"
        },
        "responses": {
          "201": {
            "description": "The new API key, with its secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewAPIKey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/account/api-keys/{key}": {
      "delete": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "deleteAPIKeyLegacy",
        "summary": "Revoke an API key",
        "deprecated": true,
        "description": "`DELETE /v1/organizations/{id}/api-keys/{key}` for the API key's Organization.",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "listSigningKeysLegacy",
        "summary": "List Signing keys",
        "deprecated": true,
        "description": "`GET /v1/namespaces/{namespace}/keys` for the API key's only Namespace.",
        "responses": {
          "200": {
            "description": "The Namespace's Signing keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SigningKeySummary"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "createSigningKeyLegacy",
        "summary": "Create a Signing key",
        "deprecated": true,
        "description": "`POST /v1/namespaces/{namespace}/keys` for the API key's only Namespace.",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateSigningKey"
        },
        "responses": {
          "201": {
            "description": "The new key, secret included.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JWK"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The Namespace already has a key with this label.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/{kid}": {
      "parameters": [
        {
          "name": "kid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "getSigningKeyLegacy",
        "summary": "Get a Signing key",
        "deprecated": true,
        "description": "`GET /v1/namespaces/{namespace}/keys/{kid}` for the API key's only Namespace.",
        "responses": {
          "200": {
            "description": "The key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JWK"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "deleteSigningKeyLegacy",
        "summary": "Delete a Signing key",
        "deprecated": true,
        "description": "`DELETE /v1/namespaces/{namespace}/keys/{kid}` for the API key's only Namespace.",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/tokens": {
      "post": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "mintClientTokenLegacy",
        "summary": "Mint a Client token",
        "deprecated": true,
        "description": "`POST /v1/namespaces/{namespace}/tokens` for the API key's only Namespace.",
        "requestBody": {
          "$ref": "#/components/requestBodies/MintClientToken"
        },
        "responses": {
          "200": {
            "description": "The signed token and what it grants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MintedToken"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/tokens/verify": {
      "post": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "verifyClientTokenLegacy",
        "summary": "Check a Client token",
        "deprecated": true,
        "description": "`POST /v1/namespaces/{namespace}/tokens/verify` for the API key's only Namespace.",
        "requestBody": {
          "$ref": "#/components/requestBodies/VerifyClientToken"
        },
        "responses": {
          "200": {
            "description": "The token is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifiedToken"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LegacyNamespace"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/enrollments": {
      "get": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "listEnrollmentsLegacy",
        "summary": "List pending enrollments",
        "deprecated": true,
        "description": "`GET /v1/organizations/{id}/enrollments` for the API key's Organization.",
        "responses": {
          "200": {
            "description": "The pending enrollments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Enrollment"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "createEnrollmentLegacy",
        "summary": "Create a Relay enrollment token",
        "deprecated": true,
        "description": "`POST /v1/organizations/{id}/enrollments` for the API key's Organization.",
        "requestBody": {
          "$ref": "#/components/requestBodies/CreateEnrollment"
        },
        "responses": {
          "201": {
            "description": "The pending enrollment, with its token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewEnrollment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "A Node already exists at this URL. Delete it first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/enrollments/{enrollment}": {
      "delete": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "deleteEnrollmentLegacy",
        "summary": "Revoke a pending enrollment",
        "deprecated": true,
        "description": "`DELETE /v1/organizations/{id}/enrollments/{enrollment}` for the API key's Organization.",
        "parameters": [
          {
            "name": "enrollment",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/nodes": {
      "get": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "listNodesLegacy",
        "summary": "List your Nodes",
        "deprecated": true,
        "description": "`GET /v1/organizations/{id}/nodes` for the API key's Organization.",
        "responses": {
          "200": {
            "description": "The Nodes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Node"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "delete": {
        "tags": [
          "Before Organizations"
        ],
        "operationId": "deleteNodeLegacy",
        "summary": "Delete a Node",
        "deprecated": true,
        "description": "`DELETE /v1/organizations/{id}/nodes` for the API key's Organization.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/Legacy"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/auth.md": {
      "get": {
        "tags": [
          "Agents"
        ],
        "operationId": "getAgentInstructions",
        "summary": "Instructions for an Agent",
        "description": "The steps an AI agent follows to register with this server's WorkOS\nAuthKit domain, get an Agent token, and use it here. Point an agent at\nthis page and it can start a trial with no help from you.\n\nThe first part is AuthKit's own instructions, copied from AuthKit at\nmost a minute ago, so they follow how AuthKit is set up. The last part\nsays what to call here with the token: list the trial, find its\nNamespace, and create an API key.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The instructions, as Markdown.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "# auth.md — agent registration\n\nYou are an agent. This service allows you to register for an identity and exchange it for credentials.\n...\n\n## 7. Use the access token with Switchboard\n...\n"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoAgents"
          },
          "502": {
            "description": "AuthKit didn't answer, or answered with an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": [
          "Agents"
        ],
        "operationId": "getProtectedResourceMetadata",
        "summary": "Where an Agent registers",
        "description": "This server's OAuth protected resource metadata (RFC 9728). It names\nthe WorkOS AuthKit domain where an Agent registers, so an agent or\nOAuth client that finds this server can get an Agent token without\nbeing told where. A 401 from any other route points here too, in its\n`WWW-Authenticate` header.\n\n`resource` is this server's public origin, the same whichever\nhostname you fetched this from. This server accepts a token issued\nfor its WorkOS client, which is what an Agent following `/auth.md`\ngets, or for exactly that resource (RFC 8707). AuthKit doesn't issue\nthe second kind: it answers `invalid_target` to a token request that\nnames a `resource`, whatever the URL, so leave it out.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "The metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProtectedResource"
                },
                "example": {
                  "resource": "https://app.caprid.com",
                  "authorization_servers": [
                    "https://auth.caprid.com"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NoAgents"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "operationId": "health",
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "description": "Up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Up, but not ready to serve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key, `sb_...`. Create one in the dashboard or with `POST /v1/organizations/{id}/api-keys`."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-switchboard_session",
        "description": "The dashboard's session cookie, set by signup and login. Only accepted\non same-origin requests, so it can't be used from another site. Over\nHTTPS it's named `__Host-switchboard_session`, which browsers accept\nonly from this host, so no other host under the same domain can plant\none. A server you run over plain HTTP names it `switchboard_session`.\n"
      },
      "agentToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The AuthKit access token an AI agent sends, on a server set up for\nAgents. The Agent registers with this server's WorkOS AuthKit domain\nand exchanges its assertion for the token, which lasts about five\nminutes. Until a person claims the Agent, the token acts in the\nAgent's Trial Organization. Once a person claims it, the token acts as\nthat person's Account, with everything their session can do, and its\nfirst request makes them the trial's Owner. The server\nchecks the token's signature against AuthKit's published keys, and\nthat AuthKit issued it for this server's WorkOS client or for this\nserver as a resource, the `resource` in\n`/.well-known/oauth-protected-resource`.\n"
      }
    },
    "parameters": {
      "OrganizationID": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The Organization's `id`, from `GET /v1/organizations`.",
        "schema": {
          "type": "integer"
        },
        "example": 4
      },
      "Namespace": {
        "name": "namespace",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/NamespaceName"
        }
      }
    },
    "requestBodies": {
      "CreateAPIKey": {
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "label": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Label"
                    }
                  ],
                  "description": "A name to tell the key apart from others. Defaults to `api`."
                },
                "expires_in": {
                  "type": "integer",
                  "minimum": 0,
                  "default": 0,
                  "description": "Seconds until the key stops working. `0` means never."
                },
                "namespaces": {
                  "type": "array",
                  "minItems": 1,
                  "items": {
                    "$ref": "#/components/schemas/NamespaceName"
                  },
                  "description": "Limits the key to these Namespaces, each a live Namespace of\nthe Organization. Leave it out for a key that reaches the\nwhole Organization. A limited key manages only the Signing\nkeys, Client tokens and settings of its Namespaces, and\ncan't create API keys. You can't change the limit later.\n"
                }
              }
            },
            "example": {
              "label": "staging-ci",
              "expires_in": 7776000,
              "namespaces": [
                "acme-staging"
              ]
            }
          }
        }
      },
      "CreateSigningKey": {
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "label": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Label"
                    }
                  ],
                  "description": "Becomes part of the `kid`, which is `<namespace>_<label>`.\nLabels are unique within a Namespace. Defaults to a random\n12-character hex string.\n"
                },
                "path": {
                  "$ref": "#/components/schemas/Path"
                },
                "publish": {
                  "$ref": "#/components/schemas/Prefixes"
                },
                "subscribe": {
                  "$ref": "#/components/schemas/Prefixes"
                }
              }
            },
            "examples": {
              "whole-namespace": {
                "summary": "A key for the whole Namespace",
                "value": {
                  "label": "backend"
                }
              },
              "publish-only": {
                "summary": "A key that can only sign tokens that publish under `live`",
                "value": {
                  "label": "ingest",
                  "publish": "live"
                }
              }
            }
          }
        }
      },
      "MintClientToken": {
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "kid": {
                  "type": "string",
                  "description": "The `kid` of the Signing key to sign with. You can leave it out when the Namespace has exactly one."
                },
                "path": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Path"
                    }
                  ],
                  "description": "Where the token is rooted, relative to the Namespace. The\ntoken's `root` claim is `<namespace>/<path>`.\n"
                },
                "publish": {
                  "$ref": "#/components/schemas/Prefixes"
                },
                "subscribe": {
                  "$ref": "#/components/schemas/Prefixes"
                },
                "expires_in": {
                  "type": "integer",
                  "minimum": 0,
                  "default": 3600,
                  "description": "Seconds until the token expires. `0` means never."
                }
              }
            },
            "examples": {
              "viewer": {
                "summary": "Watch one room for an hour",
                "value": {
                  "path": "room/42",
                  "subscribe": ""
                }
              },
              "host": {
                "summary": "Publish your own broadcast and watch everyone else's",
                "value": {
                  "path": "room/42",
                  "publish": "alice",
                  "subscribe": "",
                  "expires_in": 14400
                }
              }
            }
          }
        }
      },
      "VerifyClientToken": {
        "required": true,
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "token"
              ],
              "properties": {
                "token": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CreateEnrollment": {
        "required": true,
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The URL peers will dial this relay on, and Clients too\nunless `client_url` names another. It needs a publicly\ntrusted certificate for its host.\n"
                },
                "region": {
                  "type": "string",
                  "maxLength": 64,
                  "default": "default",
                  "description": "Which of your Regions the Node joins. Relays in the same\nRegion all peer with each other. Region names belong to\nyou, so your `iad` is unrelated to anyone else's. Can't\ncontain `/`.\n"
                },
                "gateway": {
                  "type": "boolean",
                  "default": false,
                  "description": "Whether this Node may be elected its Region's Gateway, which carries the Region's traffic to other Regions."
                },
                "client_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The Client URL, which is where Clients, such as a player\npage, reach this relay when that isn't `url`. Use it when `url`\nis a name only your private network resolves, and Clients\ncome in through a public hostname or a load balancer.\nSwitchboard doesn't route anything with it. It reports the\nvalue on the Node, for whatever hands relay addresses to\nyour Clients.\n\nIt has to start with `http://` or `https://` and can't\ncontain a username or password. Switchboard trims it and\nstores it as you send it. Leave it out, or send `\"\"`, for\nnone. The relay can't change it. If the relay's\n`connect_api` URL carries `client=`, that value has to\nmatch.\n"
                },
                "expires_in": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 86400,
                  "default": 3600,
                  "description": "Seconds until the token expires. `0` means the default. It always expires."
                }
              }
            },
            "example": {
              "url": "https://relay1.internal.acme.example:4443/",
              "region": "iad",
              "gateway": true,
              "client_url": "https://live.acme.example/"
            }
          }
        }
      }
    },
    "headers": {
      "SessionCookie": {
        "description": "`__Host-switchboard_session` over HTTPS, `switchboard_session` over plain HTTP: HttpOnly, SameSite=Lax, Path=/, valid for 14 days.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "NoWorkOS": {
        "description": "This server doesn't use WorkOS login.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoAgents": {
        "description": "This server isn't set up for Agents.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "The request is malformed or a field is invalid. The message says which.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "No valid credential. Missing, expired and revoked credentials all get\nthe same response, and so does an API key whose trial has lapsed. An\nAgent token from before a claim gets a 401 too, once the claim has\nmade the trial a person's, with a message saying to get a new token.\n",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string",
              "example": "Bearer realm=\"switchboard\""
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such thing, or not yours. Another customer's Organization or\nNamespace is a 404 too, so nobody can tell which ones exist.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Sent with an API key limited to some Namespaces, to a Namespace\noutside its limit, or to a route that acts on the Organization as a\nwhole. A limited key can never create API keys.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "this API key is limited to the Namespaces acme-staging, and acme isn't one of them"
            }
          }
        }
      },
      "AgentRefused": {
        "description": "Sent on every route with the token of an Agent that has nothing to act\nin. An Agent whose person hasn't signed up gets this until they sign\nup at `/app/`, and its next request after that acts as their new\nAccount. An Agent nobody has claimed gets this once its trial lapses,\nbecause a registration gets one trial. It can register again to start\na new one, or a person can claim it so it acts as their Account.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "no-account": {
                "summary": "The person who claimed the Agent hasn't signed up",
                "value": {
                  "error": "the person who claimed this Agent has no Switchboard Account: once they sign up at /app/, the Agent acts as their Account"
                }
              },
              "trial-over": {
                "summary": "The Agent's trial has lapsed",
                "value": {
                  "error": "this Agent's trial is over: register again to start a new one, or have a person with a Switchboard Account claim this Agent"
                }
              }
            }
          }
        }
      },
      "NotAnOwner": {
        "description": "You aren't an Owner of this Organization, or you sent an API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "only an Owner can manage this Organization's Invitations"
            }
          }
        }
      },
      "InvitationSpent": {
        "description": "The Invitation has already been used, was revoked, or has expired. Ask for a new one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "this invitation has expired"
            }
          }
        }
      },
      "Legacy": {
        "description": "Sent with a session, which may belong to several Organizations. Use the\nroute the message names instead. This route also returns 400 for\nanything that route would refuse with 400.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "this route is for API keys; use /v1/organizations/{id}/api-keys"
            }
          }
        }
      },
      "LegacyNamespace": {
        "description": "Sent with a session, or with an API key that doesn't reach exactly\none Namespace. Use the route the message names instead. This route\nalso returns 400 for anything that route would refuse with 400.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "this API key reaches 2 Namespaces, so this route can't tell which one you mean; use /v1/namespaces/{namespace}/keys"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited. Try again later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "the requested grants exceed the key's scope"
          }
        }
      },
      "NamespaceName": {
        "type": "string",
        "minLength": 3,
        "maxLength": 32,
        "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$",
        "description": "A Namespace's name, which is the root of every path signed for it.\nIt never changes. Use lowercase letters, digits and hyphens, and start\nand end with a letter or digit. Some names, like `admin` and\n`default`, are reserved, and so is every name that starts with\n`trial-`, which only Trial Organizations get.\n",
        "example": "acme"
      },
      "Label": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,64}$"
      },
      "Path": {
        "type": "string",
        "default": "",
        "description": "A path relative to your Namespace, like `room/42`. Leading, trailing\nand repeated slashes are ignored. Segments can't be `.` or `..`, or\ncontain whitespace or control characters.\n"
      },
      "Prefixes": {
        "description": "Path prefixes, relative to `path`. `\"\"` means everything under `path`.\nMatching is by whole segment: `live` covers `live/cam1` but not\n`lively`. A single string is the same as a one-item array.\n",
        "oneOf": [
          {
            "type": "string"
          },
          {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        ],
        "example": [
          "live"
        ]
      },
      "Account": {
        "type": "object",
        "required": [
          "organizations"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The person signed in. Absent for an API key."
          },
          "created_at": {
            "type": "integer",
            "description": "When the person signed up. Absent for an API key."
          },
          "api_key": {
            "allOf": [
              {
                "$ref": "#/components/schemas/APIKey"
              }
            ],
            "description": "The API key making the call, with its `namespaces` limit if it\nhas one. A limited key can't list its Organization's Namespaces,\nso this is how it finds the ones it can reach. Absent for a\nsession.\n"
          },
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organization"
            }
          },
          "namespace": {
            "allOf": [
              {
                "$ref": "#/components/schemas/NamespaceName"
              }
            ],
            "deprecated": true,
            "description": "The one Namespace you can reach, as this reported before\nOrganizations. Absent unless you reach exactly one.\n"
          },
          "platform_peering": {
            "type": "boolean",
            "deprecated": true,
            "description": "That Namespace's `platform_peering`. Absent along with `namespace`."
          }
        }
      },
      "Organization": {
        "type": "object",
        "description": "A customer of Switchboard. It owns its Namespaces, and the API keys,\nNodes and enrollments that serve them. Its Members manage all of it.\n",
        "required": [
          "id",
          "name",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Identifies the Organization in routes. It never changes.",
            "example": 4
          },
          "name": {
            "type": "string",
            "description": "For display. Any Member can change it.",
            "example": "Acme"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member"
            ],
            "description": "Your role in it. Absent for an API key, which has none."
          },
          "created_at": {
            "type": "integer"
          },
          "trial_expires_at": {
            "type": "integer",
            "description": "When a Trial Organization lapses, so its Agent knows how long it\nhas to get a person to claim it. Absent for every other\nOrganization.\n",
            "example": 1760604800
          }
        }
      },
      "Invitation": {
        "type": "object",
        "description": "An offer of membership in an Organization to one email address.",
        "required": [
          "id",
          "organization_id",
          "email",
          "role",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifies the Invitation in routes. Not a secret.",
            "example": "3f9c1a2b7d4e"
          },
          "organization_id": {
            "type": "integer",
            "example": 4
          },
          "organization_name": {
            "type": "string",
            "description": "The Organization's name, for someone who isn't in it yet.",
            "example": "Acme"
          },
          "email": {
            "type": "string",
            "description": "Who may accept it. Stored lowercased.",
            "example": "alice@acme.example"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member"
            ],
            "description": "The role they'll have once they accept."
          },
          "created_at": {
            "type": "integer",
            "example": 1790294400
          },
          "expires_at": {
            "type": "integer",
            "description": "When the link stops working, 7 days after it was created.",
            "example": 1790899200
          }
        }
      },
      "NewInvitation": {
        "description": "An Invitation just created, with its link. The link's secret is shown only here.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Invitation"
          },
          {
            "type": "object",
            "required": [
              "secret",
              "link"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "description": "The secret the link carries. Send it to `POST /v1/invitations/accept`.",
                "example": "sbi_mR3x9Qe2vT8wY1zA4bC7dF0gH5jK6lN2pS8uV3xZ1qW"
              },
              "link": {
                "type": "string",
                "description": "The dashboard page that accepts it. Send this to the person you invited.",
                "example": "https://app.caprid.com/app/invitations/sbi_mR3x9Qe2vT8wY1zA4bC7dF0gH5jK6lN2pS8uV3xZ1qW"
              }
            }
          }
        ]
      },
      "Member": {
        "type": "object",
        "description": "An Account that belongs to an Organization.",
        "required": [
          "account_id",
          "email",
          "role",
          "joined_at"
        ],
        "properties": {
          "account_id": {
            "type": "integer",
            "description": "Identifies the Member in routes.",
            "example": 12
          },
          "email": {
            "type": "string",
            "example": "alice@acme.example"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member"
            ],
            "description": "Every Member manages everything the Organization owns. An Owner\ncan also manage its Members and Invitations.\n"
          },
          "joined_at": {
            "type": "integer",
            "description": "When the Account became a Member."
          }
        }
      },
      "Namespace": {
        "type": "object",
        "required": [
          "name",
          "organization_id",
          "platform_peering",
          "created_at"
        ],
        "properties": {
          "name": {
            "$ref": "#/components/schemas/NamespaceName"
          },
          "organization_id": {
            "type": "integer",
            "description": "The Organization that owns it, for good."
          },
          "platform_peering": {
            "type": "boolean",
            "description": "Whether its tracks may travel between your Self-hosted relays and\nPlatform relays, the relays Mux runs.\n"
          },
          "created_at": {
            "type": "integer"
          }
        }
      },
      "NamespaceSettings": {
        "type": "object",
        "properties": {
          "platform_peering": {
            "type": "boolean",
            "description": "Whether the Namespace's tracks may travel between your Self-hosted\nrelays and Platform relays, the relays Mux runs. A change takes\neffect within about a minute. Your Regions can't link to Mux's yet, so for now this\nchanges nothing.\n"
          }
        }
      },
      "AccountEnvelope": {
        "type": "object",
        "required": [
          "account"
        ],
        "properties": {
          "account": {
            "$ref": "#/components/schemas/Account"
          }
        }
      },
      "APIKey": {
        "type": "object",
        "required": [
          "id",
          "label",
          "preview",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifies the key for revoking it. Not a secret.",
            "example": "3f9a1c07be42"
          },
          "label": {
            "type": "string"
          },
          "preview": {
            "type": "string",
            "description": "The first 6 characters of the secret after `sb_`, so you can tell keys apart."
          },
          "created_at": {
            "type": "integer"
          },
          "expires_at": {
            "type": "integer",
            "description": "Absent for a key that never expires."
          },
          "last_used_at": {
            "type": "integer",
            "description": "Absent for a key that has never been used."
          },
          "namespaces": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NamespaceName"
            },
            "description": "The Namespaces the key is limited to, by name. Absent for a key\nwith no limit. A deleted Namespace drops out, so a key whose\nNamespaces have all been deleted has `[]` here and can do nothing.\n",
            "example": [
              "acme-staging"
            ]
          }
        }
      },
      "NewAPIKey": {
        "allOf": [
          {
            "$ref": "#/components/schemas/APIKey"
          },
          {
            "type": "object",
            "required": [
              "secret"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "description": "The API key itself. Shown only in this response.",
                "example": "sb_R2x0Z9kQ7mVt3xLp8wNc1bYh5sDf2gJr6uEa0zWiTq4"
              }
            }
          }
        ]
      },
      "Scope": {
        "type": "object",
        "description": "What a Signing key may sign. `put` is publish and `get` is subscribe,\nas in the token claims. Both are relative to `root`.\n",
        "properties": {
          "root": {
            "type": "string",
            "example": "acme"
          },
          "put": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "get": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "JWK": {
        "type": "object",
        "description": "A Signing key in JSON Web Key form, with the `scope` extension moq\nrelays understand. `k` is the secret. Anyone holding it can sign\ntokens within the scope.\n",
        "required": [
          "alg",
          "key_ops",
          "kty",
          "k",
          "kid",
          "scope"
        ],
        "properties": {
          "alg": {
            "type": "string",
            "const": "HS256"
          },
          "key_ops": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "sign",
              "verify"
            ]
          },
          "kty": {
            "type": "string",
            "const": "oct"
          },
          "k": {
            "type": "string",
            "description": "The HMAC secret, base64url without padding."
          },
          "kid": {
            "type": "string",
            "example": "acme_backend"
          },
          "scope": {
            "$ref": "#/components/schemas/Scope"
          }
        }
      },
      "SigningKeySummary": {
        "type": "object",
        "required": [
          "kid",
          "alg",
          "scope",
          "namespace",
          "created_at"
        ],
        "properties": {
          "kid": {
            "type": "string"
          },
          "alg": {
            "type": "string",
            "const": "HS256"
          },
          "scope": {
            "$ref": "#/components/schemas/Scope"
          },
          "namespace": {
            "type": "string"
          },
          "created_at": {
            "type": "integer"
          }
        }
      },
      "Claims": {
        "type": "object",
        "description": "A Client token's payload. `root` is absolute and starts with your\nNamespace. `put` (publish) and `get` (subscribe) are prefixes relative\nto `root`.\n",
        "properties": {
          "root": {
            "type": "string",
            "example": "acme/room/42"
          },
          "put": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "get": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              ""
            ]
          },
          "exp": {
            "type": "integer",
            "description": "When the token expires. Absent for a token that never expires."
          },
          "iat": {
            "type": "integer",
            "description": "When the token was issued."
          }
        }
      },
      "MintedToken": {
        "type": "object",
        "required": [
          "token",
          "kid",
          "claims"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "A JWT. Give it to the Client, which passes it to the relay as `?jwt=`."
          },
          "kid": {
            "type": "string"
          },
          "claims": {
            "$ref": "#/components/schemas/Claims"
          }
        }
      },
      "VerifiedToken": {
        "type": "object",
        "required": [
          "kid",
          "claims"
        ],
        "properties": {
          "kid": {
            "type": "string"
          },
          "claims": {
            "$ref": "#/components/schemas/Claims"
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds left. Absent for a token that never expires."
          }
        }
      },
      "NewEnrollment": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Enrollment"
          },
          {
            "type": "object",
            "required": [
              "token"
            ],
            "properties": {
              "token": {
                "type": "string",
                "description": "The Relay enrollment token. Shown only in this response.",
                "example": "sbe_Vq3mK8nR5tWb2xYc7dLp0sHf4jGu9eAz1oQi6vTk3wM"
              }
            }
          }
        ]
      },
      "Enrollment": {
        "type": "object",
        "required": [
          "id",
          "account",
          "url",
          "canonical",
          "region",
          "gateway",
          "preview",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifies the enrollment for revoking it. Not the token."
          },
          "account": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Organization's oldest Namespace, kept from before\nOrganizations. The Node belongs to the whole Organization and\ncarries all of its Namespaces. `null` when the Organization has no\nNamespaces.\n"
          },
          "url": {
            "type": "string",
            "description": "The URL as you sent it."
          },
          "canonical": {
            "type": "string",
            "description": "The URL in the normalized form Switchboard uses to tell Nodes apart."
          },
          "region": {
            "type": "string"
          },
          "gateway": {
            "type": "boolean"
          },
          "client_url": {
            "type": "string",
            "description": "The Client URL the Node will have. Omitted when it has none."
          },
          "preview": {
            "type": "string",
            "description": "The first 6 characters of the token after `sbe_`."
          },
          "created_at": {
            "type": "integer"
          },
          "expires_at": {
            "type": "integer"
          }
        }
      },
      "Node": {
        "type": "object",
        "required": [
          "url",
          "canonical",
          "source",
          "account",
          "region",
          "gateway",
          "enrolled",
          "first_seen_at",
          "last_seen_at",
          "live"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "The URL the Node's peers dial, as it was enrolled."
          },
          "canonical": {
            "type": "string",
            "description": "The URL in the normalized form Switchboard uses to tell Nodes apart."
          },
          "source": {
            "type": "string",
            "enum": [
              "poll",
              "static"
            ]
          },
          "account": {
            "type": [
              "string",
              "null"
            ],
            "description": "The Organization's oldest Namespace, kept from before\nOrganizations. The Node belongs to the whole Organization and\ncarries all of its Namespaces. `null` when the Organization has no\nNamespaces.\n"
          },
          "region": {
            "type": "string"
          },
          "gateway": {
            "type": "boolean"
          },
          "client_url": {
            "type": "string",
            "description": "The Client URL, which is where Clients, such as a player page,\nreach this relay when that isn't `url`. On a private network, `url` is often\na name no Client can resolve, so whatever hands relay addresses to\nClients reads this instead. It is fixed when the Node is enrolled,\nand omitted when the Node has none. Relays never see it, because\npeer discovery hands out only `url`.\n"
          },
          "enrolled": {
            "type": "boolean",
            "description": "Whether the Node came from a Relay enrollment token, so its relay has a Node key."
          },
          "first_seen_at": {
            "type": "integer"
          },
          "last_seen_at": {
            "type": "integer",
            "description": "When its relay last polled, or when it enrolled if it hasn't polled since."
          },
          "live": {
            "type": "boolean",
            "description": "Whether its relay has polled in the last 90 seconds."
          }
        }
      },
      "ProtectedResource": {
        "type": "object",
        "required": [
          "resource",
          "authorization_servers"
        ],
        "properties": {
          "resource": {
            "type": "string",
            "format": "uri",
            "description": "This server's public origin, the one resource its Agent tokens may name."
          },
          "authorization_servers": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "The WorkOS AuthKit domain an Agent registers with."
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "unavailable"
            ]
          }
        },
        "additionalProperties": true
      }
    }
  }
}
