{
  "openapi": "3.1.0",
  "info": {
    "title": "Vitrina Engine API",
    "version": "1",
    "description": "Uptime and infrastructure monitoring: monitors, the incidents they open, the\nerrors your applications report, and the analytics for the sites you measure.\n\n## Authentication\n\nEvery request carries an API key as a bearer token. Keys are minted per organisation at\n`/settings/api-keys` and carry the role and workspace confinement of the member who made\nthem — so two keys on one account can legitimately see different monitors. `GET /api/v1`\nreports what the key in hand can see, which is the cheapest way to tell a confined key from\na missing row.\n\n## Scope\n\nA key never chooses an organisation. The organisation is resolved from the key and every\nquery is filtered by it, so there is no parameter anywhere for somebody else's data.\n\n## Refusals\n\nEvery non-2xx body is the same shape: an `error` string, a `code`, `vars` where the\nsentence names a value, and `details` where a field can be named. A reference that belongs to\nanother organisation answers **404**, never 403 — the response cannot be used to find out\nwhether an id exists.\n\n**Branch on `code`, never on `error`.** The sentence is English prose for a person reading a\nlog and may be reworded at any time; the code is the stable identity of the refusal and is\nwhat our own apps translate. `vars` carries the values the sentence interpolates — the plan\nand the limit, the field that was missing — as strings, so a client can put them where its own\ngrammar wants them.\n\n## For agents\n\n`POST https://vitrinaengine.com/mcp` is a Model Context Protocol server over the same key and\nthe same data: every `GET` below is offered as a tool, together with creating, changing and\ndeleting a monitor — and nothing else that writes. An agent cannot reach a workspace, a\nmember, a key or an account through it, because no tool for any of those exists. A tool call\nis the same request the key would make directly, so the key's role still decides: give an\nagent a read-only key and it reads. An agent that has a key and reads `GET /api/v1` finds the\naddress under `mcp`.\n\n## Regions\n\nNothing here names the region a check ran from. Where a check runs is ours to decide and to\nchange, so it is not part of the contract.",
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://vitrinaengine.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "account"
    },
    {
      "name": "agents"
    },
    {
      "name": "analytics"
    },
    {
      "name": "api-keys"
    },
    {
      "name": "audit"
    },
    {
      "name": "billing"
    },
    {
      "name": "channels"
    },
    {
      "name": "errors"
    },
    {
      "name": "export"
    },
    {
      "name": "hosts"
    },
    {
      "name": "incidents"
    },
    {
      "name": "invitations"
    },
    {
      "name": "maintenance"
    },
    {
      "name": "me"
    },
    {
      "name": "members"
    },
    {
      "name": "monitors"
    },
    {
      "name": "on-call"
    },
    {
      "name": "organizations"
    },
    {
      "name": "probes"
    },
    {
      "name": "search"
    },
    {
      "name": "setup"
    },
    {
      "name": "sms"
    },
    {
      "name": "sourcemaps"
    },
    {
      "name": "status-pages"
    },
    {
      "name": "summary"
    },
    {
      "name": "symbols"
    },
    {
      "name": "workspaces"
    }
  ],
  "paths": {
    "/api/v1/account": {
      "delete": {
        "operationId": "delete_account",
        "tags": [
          "account"
        ],
        "summary": "Delete your own account.",
        "description": "`DELETE /api/v1/account` — delete your own account.\n\nApp Store guideline 5.1.1(v): an app that offers account creation must offer account deletion\nfrom inside the app, and pointing at a support address does not satisfy it. The mobile app\nauthenticates with a session rather than a key, so this lives in v1 beside `/me`, which is the\nother endpoint that exists for the app rather than for a script.\n\n## Session only, never an API key\n\nThe same rule as `/api/push/devices`, and here it matters more. A key is issued against a\nmembership and is meant to be shared with a script; it is routinely pasted into CI. **A\ncredential a colleague can read must not be able to destroy the person who minted it** — the\nactor behind a key is its creator, so a key in a pipeline would delete their account and every\norganisation membership with it.\n\nChecked on `api_key_id`, which is set for every key and `None` for a session. Not on the\nscopes: those are `None` for an unscoped key as well as for a session, which is the same\nmistake `/api/v1/me` had to avoid when it decided whether to list the other organisations.\n\n## There is no permission check\n\nDeliberate, and not an omission. Every role can delete **their own** account; what is being\ndestroyed is the caller, and a `viewer` who could not leave would be a person the product\nholds on to. The two refusals below are about the state of the account, not about a role.\n\nRate limit: 30 writes a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountDeletedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AccountDeletedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/agents": {
      "get": {
        "operationId": "agents",
        "tags": [
          "agents"
        ],
        "summary": "The server agents this account has enrolled.",
        "description": "`GET /api/v1/agents` — the server agents this account has enrolled.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListAgentsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListAgentsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "enrol_agent",
        "tags": [
          "agents"
        ],
        "summary": "Enrol one.",
        "description": "`POST /api/v1/agents` — enrol one.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.EnrolAgentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrolledAgentResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/EnrolledAgentResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "retire_agent",
        "tags": [
          "agents"
        ],
        "summary": "`DELETE /api/v1/agents?id=...`, or `{ \"id\": \"...\" }` — retire one.",
        "description": "`DELETE /api/v1/agents?id=...`, or `{ \"id\": \"...\" }` — retire one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.RetireAgentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/agents/{id}/metrics": {
      "get": {
        "operationId": "agent_metrics",
        "tags": [
          "agents"
        ],
        "summary": "An agent's recent samples.",
        "description": "`GET /api/v1/agents/{id}/metrics` — an agent's recent samples.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetAgentMetricsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetAgentMetricsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/analytics": {
      "get": {
        "operationId": "analytics",
        "tags": [
          "analytics"
        ],
        "summary": "The sites this account measures.",
        "description": "`GET /api/v1/analytics` — the sites this account measures.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListAnalyticsSitesResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListAnalyticsSitesResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/analytics/{id}": {
      "get": {
        "operationId": "analytics_site",
        "tags": [
          "analytics"
        ],
        "summary": "One site's traffic.",
        "description": "`GET /api/v1/analytics/{id}` — one site's traffic.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetAnalyticsSiteResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetAnalyticsSiteResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "operationId": "api_keys",
        "tags": [
          "api-keys"
        ],
        "summary": "The keys, by prefix.",
        "description": "`GET /api/v1/api-keys` — the keys, by prefix.\n\nThe token comes back exactly once, from `POST`. Only its SHA-256 is stored; a lost token is\nre-issued rather than recovered, and the prefix here is enough to identify a key to a human\nand useless to anybody else.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListApiKeysResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListApiKeysResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_api_key",
        "tags": [
          "api-keys"
        ],
        "summary": "Mint one.",
        "description": "`POST /api/v1/api-keys` — mint one.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiKeyResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiKeyResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "revoke_api_key",
        "tags": [
          "api-keys"
        ],
        "summary": "Revoke one, by `?id=` or `{\"id\": \"...\"}`.",
        "description": "`DELETE /api/v1/api-keys` — revoke one, by `?id=` or `{\"id\": \"...\"}`.\n\nThe id is a query parameter rather than a path segment because that is where the TypeScript\nroute takes it: `/api/v1/api-keys` has no `[id]` directory. Moving it would be a new URL,\nand this is a port.\n\nA body is accepted as well, because several HTTP clients make a DELETE with a query string\nawkward and one with a body easy. The query parameter is the documented form.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.RevokeApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/audit": {
      "get": {
        "operationId": "audit",
        "tags": [
          "audit"
        ],
        "summary": "Who changed what, and when.",
        "description": "`GET /api/v1/audit` — who changed what, and when.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListAuditResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListAuditResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/billing": {
      "get": {
        "operationId": "billing",
        "tags": [
          "billing"
        ],
        "summary": "What the organisation is paying, and how.",
        "description": "`GET /api/v1/billing` — what the organisation is paying, and how.\n\nThe only endpoint that answers \"where does this subscription live\", which decides what a\nclient's plan screen may offer: an Apple subscriber is sent to their App Store subscription\nsettings, a Paddle subscriber to the web, and neither is ever shown a buy button for the\nother. Apple permits telling somebody their account is already subscribed; it does not\npermit linking out to buy.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetBillingResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetBillingResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/billing/apple/verify": {
      "post": {
        "operationId": "apple_verify",
        "tags": [
          "billing"
        ],
        "summary": "Turn a StoreKit transaction id into an entitlement.",
        "description": "`POST /api/v1/billing/apple/verify` — turn a StoreKit transaction id into an entitlement.\n\nThe app calls this on purchase, on restore, and on any cold start where the StoreKit\nentitlement disagrees with the session, so it has to be idempotent — it is, because the\nsubscription row is keyed on Apple's original transaction id and every write is guarded on\nthe signed date.\n\n**The body carries an id and nothing else that is believed.** The product, the plan, the\nexpiry, the environment and the bundle are all read back from the App Store Server API over a\nsigned channel. A client that could name its own plan here would be a client that grants\nitself Business for free.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.VerifyAppleRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppleVerifyResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AppleVerifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/channels": {
      "get": {
        "operationId": "channels",
        "tags": [
          "channels"
        ],
        "summary": "Where alerts go.",
        "description": "`GET /api/v1/channels` — where alerts go.\n\nTwo rules govern what comes out of here and neither is negotiable. A channel is listed with\nits **owner**, because whoever routes an alert is usually not the person it wakes. And a\nsealed secret never leaves: the response carries the non-secret fields of `config` plus a\nboolean saying whether a secret is stored.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListChannelsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListChannelsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_channel",
        "tags": [
          "channels"
        ],
        "summary": "Add a destination for alerts.",
        "description": "`POST /api/v1/channels` — add a destination for alerts.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateChannelRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedChannelResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedChannelResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/channels/{id}": {
      "patch": {
        "operationId": "patch_channel",
        "tags": [
          "channels"
        ],
        "summary": "Enable or disable one.",
        "description": "`PATCH /api/v1/channels/{id}` — enable or disable one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateChannelRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "delete_channel",
        "tags": [
          "channels"
        ],
        "summary": "Remove one.",
        "description": "`DELETE /api/v1/channels/{id}` — remove one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/channels/{id}/confirmation": {
      "post": {
        "operationId": "resend_channel_confirmation",
        "tags": [
          "channels"
        ],
        "summary": "Send a channel's confirmation email again.",
        "description": "`POST /api/v1/channels/{id}/confirmation` — send a channel's confirmation email again.\n\nNeeds `notification_channel:manage` on the channel's own workspace, resolved before the check\nso a crafted id is \"Not found.\" rather than a send from a stranger's channel. **A new token\nevery time**, and the old link stops working.\n\n`sent: false` when there is nothing to send — the address already confirmed, or a kind that\nnever asks — which is an answer rather than a refusal. It is never a way to mail a confirmed\naddress again: that would make this a button that mails an arbitrary address on demand, which\nis what double opt-in exists to prevent. A send the mail provider refused is 502,\n`confirmation_not_sent`; the token was still rotated, so pressing again is the fix.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelConfirmationResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelConfirmationResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/channels/{id}/test": {
      "post": {
        "operationId": "test_channel",
        "tags": [
          "channels"
        ],
        "summary": "Send a real message through a channel.",
        "description": "`POST /api/v1/channels/{id}/test` — send a real message through a channel.\n\nThe only way to find out whether a channel works before an outage does, and the reason it is\nworth an endpoint of its own on a phone: somebody who has just added a destination will test\nit from wherever they are standing, not from a desk.\n\nCharged against the **write** bucket rather than the read one. It sends actual mail and makes\nan actual request to a customer-supplied URL, so a loop here costs our sending reputation and\nsomebody else's endpoint — that is a write whatever the row says.\n\n## A failed delivery is a 200\n\n`{\"delivered\": false, \"error\": \"...\"}`, not a 4xx or a 5xx. The request succeeded: we asked\nthe channel to take a message and it refused, which is exactly the answer the caller wanted.\nReporting it as an error status would make a client retry a test that is telling it the truth,\nand would hide the provider's own words behind a status code that says nothing about them.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TestDeliveryResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/TestDeliveryResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/errors/issues": {
      "get": {
        "operationId": "error_issues",
        "tags": [
          "errors"
        ],
        "summary": "Grouped errors, newest activity first.",
        "description": "`GET /api/v1/errors/issues` — grouped errors, newest activity first.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "project",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListIssuesResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListIssuesResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/errors/issues/{id}": {
      "get": {
        "operationId": "error_issue",
        "tags": [
          "errors"
        ],
        "summary": "One issue, with the stack as the customer's own source.",
        "description": "`GET /api/v1/errors/issues/{id}` — one issue, with the stack as the customer's own source.\n\n## The frames are resolved here, not left minified\n\n`main.4f3a.js:1:88213` tells nobody anything, and an issue endpoint that returned it would\nhand an integrator exactly the problem the source-map feature exists to solve. So this runs\nthe same resolution the dashboard does, against the same uploaded artefacts — a client cannot\nbe looking at a different stack from the customer it is talking to.\n\nResolved on read rather than at ingest: ingest is the hot path and the endpoint with the\nweakest authentication, and a map uploaded *after* the errors arrived still helps, which is\nthe usual order. A failure falls back to the original frames, because an unreadable stack is\nthe behaviour that existed before maps and is the honest thing to return.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetIssueResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetIssueResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patch_error_issue",
        "tags": [
          "errors"
        ],
        "summary": "Resolve, ignore, or reopen.",
        "description": "`PATCH /api/v1/errors/issues/{id}` — resolve, ignore, or reopen.\n\nThe release bookkeeping, the tenancy check and the audit entry happen in one place, so an\nissue resolved by a script is indistinguishable afterwards from one resolved by a person.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateIssueRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssueStatusResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/IssueStatusResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/errors/projects": {
      "get": {
        "operationId": "error_projects",
        "tags": [
          "errors"
        ],
        "summary": "The error-tracking projects this account holds.",
        "description": "`GET /api/v1/errors/projects` — the error-tracking projects this account holds.\n\nRead-only, deliberately: creating a project hands back a `publicKey` that still has to be\npasted into a DSN in somebody's application config, so automating it does the easy half of\na two-step job. What is worth an endpoint is seeing what is broken.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListErrorProjectsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListErrorProjectsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export": {
      "get": {
        "operationId": "export_all",
        "tags": [
          "export"
        ],
        "summary": "Everything this key may read, as a zip.",
        "description": "`GET /api/v1/export` — everything this key may read, as a zip.\n\nOne file per entity in the format `format` asks for, plus a `manifest.json` recording what\nthe archive holds, how many rows each file has and which entities are absent.\n\nEntities the key's role cannot read are **left out rather than refused**: an agency key\nscoped to one workspace should still be able to take that workspace's data, and failing the\nwhole export because one entity is out of reach would mean the narrower the key, the less\nportable the account. The manifest is what stops that being silent — an absent file and an\nempty account are otherwise the same thing.\n\nBecause the archive covers every entity at once, the hourly series are not in it: they are\nwindow-capped and would refuse the request for everything else too. Ask for\n`/export/uptime-hourly` with a window when you want them.\n\nRate limit: 6 exports an hour, per organisation.",
        "parameters": [
          {
            "name": "bom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A zip: one file per entity, plus `manifest.json` recording what is in it.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 6 exports an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export/{entity}": {
      "get": {
        "operationId": "export_entity",
        "tags": [
          "export"
        ],
        "summary": "One kind of row, as CSV or JSON.",
        "description": "`GET /api/v1/export/{entity}` — one kind of row, as CSV or JSON.\n\nThe entity is one of `monitors`, `hosts`, `status-pages`, `notification-channels`,\n`members`, `incidents`, `incident-events`, `postmortems`, `audit-log`, `error-issues`,\n`uptime-daily`, `uptime-hourly` or `analytics-hourly`. An entity this endpoint does not\nserve is **404**, the same answer a row belonging to somebody else gets.\n\n`format` is `csv` (the default) or `json`. `from` and `to` are RFC 3339 timestamps bounding\nthe entity's own time column. `bom` prepends a UTF-8 byte-order mark to a CSV, which is what\nExcel needs to read a non-ASCII name correctly and what most scripting languages read as\npart of the first column heading — so it is off unless asked for.\n\nEach entity is read under the permission its own screen asks for: exporting the audit log\nneeds `audit_log:read`, channels need `notification_channel:read`. An export is never a way\nround a permission, and a member confined to a workspace exports only that workspace.\n\nThe body is **streamed**, so it starts arriving before the whole result has been read out of\nthe database, and no part of the account is ever resident in full. The hourly series are\ncapped at 92 days per request; the daily one carries whatever history the plan kept.\nUptime comes from the rollups, which outlive the raw checks a retention sweep deletes.\n\nRate limit: 6 exports an hour, per organisation.",
        "parameters": [
          {
            "name": "entity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rows, as CSV or as a JSON array — whichever `format` asked for.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 6 exports an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/hosts": {
      "get": {
        "operationId": "hosts",
        "tags": [
          "hosts"
        ],
        "summary": "The machines, with what each one's monitors say about it.",
        "description": "`GET /api/v1/hosts` — the machines, with what each one's monitors say about it.\n\nHosts are an **optional** grouping: an account that has never created one gets an empty\nlist here and loses nothing anywhere else. Where they exist, `status` is derived only from\nmonitors pinned to a single host — see `crates/core/src/hosts.rs` for why a load-balanced\nmonitor must not colour a machine it is one of four behind.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListHostsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListHostsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_host",
        "tags": [
          "hosts"
        ],
        "summary": "Record a machine.",
        "description": "`POST /api/v1/hosts` — record a machine.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateHostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/hosts/{id}": {
      "get": {
        "operationId": "host",
        "tags": [
          "hosts"
        ],
        "summary": "One machine and the monitors on it.",
        "description": "`GET /api/v1/hosts/{id}` — one machine and the monitors on it.\n\nBoth groups in one response. The split between \"only here\" and \"shared with other hosts\" is\nthe whole point of the resource, and a client that had to fetch them separately would show a\nmachine's health without the monitors that deliberately do not decide it.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetHostResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetHostResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patch_host",
        "tags": [
          "hosts"
        ],
        "summary": "Rename one, or change the operator's own notes on it.",
        "description": "`PATCH /api/v1/hosts/{id}` — rename one, or change the operator's own notes on it.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateHostRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "delete_host",
        "tags": [
          "hosts"
        ],
        "summary": "Retire a machine.",
        "description": "`DELETE /api/v1/hosts/{id}` — retire a machine.\n\nEvery monitor on it keeps running. A host owns no history and no schedule: removing one\ntakes the attribution away and changes nothing about what is checked, which is why there is\nno emptiness check to pass first.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/hosts/{id}/monitors": {
      "post": {
        "operationId": "add_host_monitor",
        "tags": [
          "hosts"
        ],
        "summary": "Put one monitor on this machine.",
        "description": "`POST /api/v1/hosts/{id}/monitors` — put one monitor on this machine.\n\nOne monitor at a time rather than a whole list, and that is the deliberate opposite of\n`PUT /api/v1/monitors/{id}/hosts`. A host on an agency account carries monitors from several\nclients, so a caller replacing \"the list\" would be replacing rows it was never shown.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SetHostMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "remove_host_monitor",
        "tags": [
          "hosts"
        ],
        "summary": "Take one monitor off this machine.",
        "description": "`DELETE /api/v1/hosts/{id}/monitors` — take one monitor off this machine.\n\nBy `?monitorId=`, or the same key in a body. A query parameter as well, because DELETE with\na body is refused or dropped by enough HTTP clients that relying on it would make the\nendpoint unreachable from a caller with no way around it.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "monitorId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SetHostMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/incidents": {
      "get": {
        "operationId": "incidents",
        "tags": [
          "incidents"
        ],
        "summary": "Incidents, newest first. `?open=true` for the ones that matter.",
        "description": "`GET /api/v1/incidents` — incidents, newest first. `?open=true` for the ones that matter.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "open",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListIncidentsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListIncidentsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/incidents/{id}": {
      "get": {
        "operationId": "incident",
        "tags": [
          "incidents"
        ],
        "summary": "One incident.",
        "description": "`GET /api/v1/incidents/{id}` — one incident.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patch_incident",
        "tags": [
          "incidents"
        ],
        "summary": "Acknowledge, resolve, or comment.",
        "description": "`PATCH /api/v1/incidents/{id}` — acknowledge, resolve, or comment.\n\nA status word rather than two endpoints. Acknowledging and resolving are the same kind of\nact — a human taking responsibility for an incident at a point on its lifecycle — and a\ncaller that has to pick between `/ack` and `/resolve` gains nothing for the extra URL.\n\nThe comment is written **first**, before any status change. If both are sent and the status\nchange fails, the note explaining *why* somebody was closing the incident is still on the\ntimeline; the other order loses it, and the note is the part a human reads afterwards.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateIncidentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/incidents/{id}/events": {
      "get": {
        "operationId": "incident_events",
        "tags": [
          "incidents"
        ],
        "summary": "What happened, in order, and who did it.",
        "description": "`GET /api/v1/incidents/{id}/events` — what happened, in order, and who did it.\n\nThe dashboard has no screen for this. That gap matters most on a phone, because the person\nreading it at three in the morning is deciding whether anybody else has already picked the\npage up — \"escalated to step 2, then acknowledged\" is the whole answer.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListIncidentEventsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListIncidentEventsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/incidents/{id}/postmortem": {
      "put": {
        "operationId": "put_incident_postmortem",
        "tags": [
          "incidents"
        ],
        "summary": "Write or replace the postmortem of a resolved incident.",
        "description": "`PUT /api/v1/incidents/{id}/postmortem` — write or replace the postmortem of a resolved incident.\n\nMarkdown in `body`, trimmed, at most 50,000 characters. Needs `incident:resolve` on the\nincident's workspace: whoever may declare an incident over may write down why it happened.\nRefused with `incident_not_resolved` while the incident is still open. Answers with the\nincident as `GET /incidents/{id}` would, postmortem included.\n\nA whole document, not a patch — there is one per incident and the last save is the\npostmortem. It is internal and never appears on a status page.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SavePostmortemRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetIncidentResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invitations/{id}": {
      "delete": {
        "operationId": "revoke_invitation",
        "tags": [
          "invitations"
        ],
        "summary": "Withdraw one.",
        "description": "`DELETE /api/v1/invitations/{id}` — withdraw one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/maintenance": {
      "get": {
        "operationId": "maintenance",
        "tags": [
          "maintenance"
        ],
        "summary": "The scheduled windows.",
        "description": "`GET /api/v1/maintenance` — the scheduled windows.\n\n`incident:read`, not a configuration permission: reading a window is reading the incident\nstory, so a responder can see why they were not paged.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "past",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMaintenanceResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMaintenanceResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_maintenance",
        "tags": [
          "maintenance"
        ],
        "summary": "Schedule a window.",
        "description": "`POST /api/v1/maintenance` — schedule a window.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateMaintenanceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "cancel_maintenance",
        "tags": [
          "maintenance"
        ],
        "summary": "Cancel a window.",
        "description": "`DELETE /api/v1/maintenance?id=...` — cancel a window.\n\nCancelled, not deleted. The notifier reads `cancelled_at is null`, so this is what ends a\nwindow — and the row surviving is what lets somebody answer \"was that outage suppressed on\npurpose?\" long afterwards.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "me",
        "tags": [
          "me"
        ],
        "summary": "Who is asking, and everything a client needs to render a shell.",
        "description": "`GET /api/v1/me` — who is asking, and everything a client needs to render a shell.\n\nThe mobile app cannot build its navigation without this: which organisation it is in, what\nrole the person holds, what the plan entitles them to, and how much of each limit is spent.\nDeliberately one response rather than several — it is fetched on every cold start, and a\nphone on a bad connection pays for round trips, not for bytes.\n\nNo permission check, and that is not an omission. Every field here describes the caller's\nown credential; a key with no scopes at all still has to be able to ask what it is.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMeResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMeResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/members": {
      "get": {
        "operationId": "members",
        "tags": [
          "members"
        ],
        "summary": "Everyone with access, and every invitation still outstanding.",
        "description": "`GET /api/v1/members` — everyone with access, and every invitation still outstanding.\n\nOne response rather than two endpoints. A pending invitation is a seat that is already\nspent, so a client that fetched only the members would show a seat count that disagrees\nwith the one the invite is refused on.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMembersResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMembersResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "invite_member",
        "tags": [
          "members"
        ],
        "summary": "Invite somebody.",
        "description": "`POST /api/v1/members` — invite somebody.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.InviteMemberRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/members/{id}": {
      "patch": {
        "operationId": "patch_member",
        "tags": [
          "members"
        ],
        "summary": "Change somebody's role.",
        "description": "`PATCH /api/v1/members/{id}` — change somebody's role.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateMemberRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "remove_member",
        "tags": [
          "members"
        ],
        "summary": "Remove somebody.",
        "description": "`DELETE /api/v1/members/{id}` — remove somebody.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors": {
      "get": {
        "operationId": "monitors",
        "tags": [
          "monitors"
        ],
        "summary": "Every monitor this key can see.",
        "description": "`GET /api/v1/monitors` — every monitor this key can see.\n\n`?tag=prod,eu` narrows the list to monitors carrying **every** tag named — AND, not OR.\nSeveral tags therefore always return fewer monitors than one does, which is what pressing a\nsecond filter looks like it should do; OR would return more. The dashboard's filter reads\nexactly the same way, so a `?tag=` link means the same thing in both.\n\nTags are matched as they are stored: folded to lower case, trimmed, with inner whitespace\ncollapsed. The value is normalised the same way before it is matched, so `?tag=Prod` finds\nthem. An entry that cannot be a tag — blank, or longer than a tag may be — is dropped and\nthe rest of the filter still applies, the same way `?days=` is clamped rather than refused:\na link pasted into a ticket should not 400 because one of its tags was renamed.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_monitor",
        "tags": [
          "monitors"
        ],
        "summary": "Create a monitor.",
        "description": "`POST /api/v1/monitors` — create a monitor.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}": {
      "get": {
        "operationId": "monitor",
        "tags": [
          "monitors"
        ],
        "summary": "One monitor.",
        "description": "`GET /api/v1/monitors/{id}` — one monitor.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patch_monitor",
        "tags": [
          "monitors"
        ],
        "summary": "Change one, pause one, or both.",
        "description": "`PATCH /api/v1/monitors/{id}` — change one, pause one, or both.\n\nPausing is checked and applied **first and separately**: it is its own permission, and a key\nallowed to pause but not to update should still be able to do the thing it is allowed to do.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedMonitorResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedMonitorResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "delete_monitor",
        "tags": [
          "monitors"
        ],
        "summary": "Retire a monitor.",
        "description": "`DELETE /api/v1/monitors/{id}` — retire a monitor.\n\nThe first write, and the shape every other one takes. Four steps in a fixed order:\n\n1. `guard` resolves the caller and charges the **write** bucket, not the read one.\n2. The organisation-level permission, so a key with no `monitor:delete` scope is refused\n   before anything is loaded.\n3. `with_resource` loads the row and checks the permission **against the workspace that row\n   actually has** — the rule `setMonitorChannels` once got wrong, where an empty resource\n   passed for any authenticated editor and let the id in the URL pick the row.\n4. The change and its audit entry, in one transaction.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}/channels": {
      "get": {
        "operationId": "monitor_channels",
        "tags": [
          "monitors"
        ],
        "summary": "Which channels a monitor alerts through.",
        "description": "`GET /api/v1/monitors/{id}/channels` — which channels a monitor alerts through.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorChannelsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorChannelsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "set_monitor_channels",
        "tags": [
          "monitors"
        ],
        "summary": "Set the whole list.",
        "description": "`PUT /api/v1/monitors/{id}/channels` — set the whole list.\n\n`PUT`, not `PATCH`, because that is what it is: the body replaces every attachment on the\nmonitor. There is no add-one or remove-one form, deliberately — a client that reads the list,\nedits it and writes it back cannot race itself into a half-applied routing change, which for\nthis particular setting means an alert going nowhere.\n\nIds that are not this organisation's are **dropped rather than refused**, which is what the\ndashboard does: refusing would leak whether an id exists somewhere else. The response reads\nthe list back so a caller can compare it against what they sent.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SetMonitorChannelsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorChannelsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorChannelsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}/graph": {
      "get": {
        "operationId": "monitor_graph",
        "tags": [
          "monitors"
        ],
        "summary": "One monitor's last hours, hour by hour.",
        "description": "`GET /api/v1/monitors/{id}/graph` — one monitor's last hours, hour by hour.\n\nThe chart on the dashboard's monitor page: `?hours=` defaults to 24 and is clamped to a week.\nAn `uptime` graph carries the share of each hour's checks that passed, null for an hour with\nnone; a `value` graph carries every series the monitor measures, each padded onto the same\nhours, with its unit — `percent` is drawn as it stands and `raw` against its own highest\nreading in the window.\n\nA separate path from `/history` rather than an `?hours=` switch on it: that route answers with\ndaily uptime, and one path answering two shapes depending on a query parameter is a contract no\ngenerated client can describe.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorGraphResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorGraphResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}/history": {
      "get": {
        "operationId": "monitor_history",
        "tags": [
          "monitors"
        ],
        "summary": "Daily uptime, for the bar a client draws.",
        "description": "`GET /api/v1/monitors/{id}/history` — daily uptime, for the bar a client draws.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorHistoryResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetMonitorHistoryResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}/hosts": {
      "get": {
        "operationId": "monitor_hosts",
        "tags": [
          "monitors"
        ],
        "summary": "The machines one monitor runs on.",
        "description": "`GET /api/v1/monitors/{id}/hosts` — the machines one monitor runs on.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorHostsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorHostsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "set_monitor_hosts",
        "tags": [
          "monitors"
        ],
        "summary": "The whole list, for one monitor.",
        "description": "`PUT /api/v1/monitors/{id}/hosts` — the whole list, for one monitor.\n\nSafe as a whole list where the host side is not: these links belong to this monitor, so\nthere is nothing on the other end of them that another workspace can see. An empty array is\naccepted and means \"no host in particular\", which is where most monitors are.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SetMonitorHostsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorHostsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorHostsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/{id}/push-url": {
      "get": {
        "operationId": "monitor_push_url",
        "tags": [
          "monitors"
        ],
        "summary": "A heartbeat monitor's ping URL, shown again.",
        "description": "`GET /api/v1/monitors/{id}/push-url` — a heartbeat monitor's ping URL, shown again.\n\nNeeds `monitor:read`, which is what the dashboard shows it to: anybody who can see the\nmonitor. The token is stored **sealed** so the URL can be displayed again — it lives in a\ncrontab on a machine somebody may be rebuilding — while pings are matched on its hash, so the\nstored value is never what authenticates anything.\n\nBoth stored forms are read, as the dashboard reads them: sealed, and the plaintext a row can\nstill hold until `secrets:reseal` has run. `url` is null for a kind that is not pinged, and for\na sealed token no key in the ring can open — the monitor keeps working, and rotating the token\nissues a URL that can be shown.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetPushUrlResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetPushUrlResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitors/graphs": {
      "get": {
        "operationId": "monitor_graphs",
        "tags": [
          "monitors"
        ],
        "summary": "The last hours of every monitor this key can see, hour by hour.",
        "description": "`GET /api/v1/monitors/graphs` — the last hours of every monitor this key can see, hour by hour.\n\nWhat the dashboard's monitor list draws on each row, in one response for the whole list.\n`?hours=` defaults to 24 and is clamped to a week.\n\nA collection rather than a request per row, for the reason every v1 collection is one: a\nlist of two hundred monitors would otherwise be two hundred round trips on a phone and would\nspend the read bucket's minute before the list had drawn. Server-side it is three statements\nwhatever the count, which is exactly how the dashboard reads it.\n\nAn `uptime` graph carries the share of each hour's checks that passed; a `value` graph — the\nquantitative kinds — carries its readings as series, and a client scales them itself. Hourly\nrollups only.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorGraphsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListMonitorGraphsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/on-call": {
      "get": {
        "operationId": "on_call",
        "tags": [
          "on-call"
        ],
        "summary": "Who is on call right now, the rotations behind that, and the escalation policies.",
        "description": "Who is on call right now, the rotations behind that, and the escalation policies.\n\nThe rotation is **computed from the clock**, never stored, and it is computed by\n`on_call_now` — the same function the notifier pages through. That sharing is the whole\npoint of reading it from there rather than working it out here: an API that named a\ndifferent person from the one the pager actually reaches would be worse than no API,\nbecause this is the answer somebody checks before going to bed.\n\nRead-only. Creating a schedule means picking participants, a timezone and a rotation start,\nwhich is a form rather than something to automate, and getting it wrong builds a pager that\nsilently pages nobody. The dashboard keeps that job.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetOnCallResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetOnCallResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/organizations": {
      "get": {
        "operationId": "organizations",
        "tags": [
          "organizations"
        ],
        "summary": "Every organisation the signed-in person belongs to.",
        "description": "`GET /api/v1/organizations` — every organisation the signed-in person belongs to.\n\n**Session only.** A key is issued against one membership and confined to it, so listing the\nothers would advertise organisations that credential cannot reach — the same reason\n`/api/v1/me` returns an empty `organizations` for a key. Checked on `apiKeyId`, never on the\nscopes, which are null for an unscoped key as well as for a session.\n\n`active` marks the one this session currently resolves to.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListOrganizationsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListOrganizationsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/organizations/active": {
      "put": {
        "operationId": "switch_organization",
        "tags": [
          "organizations"
        ],
        "summary": "Move this person to another organisation they belong to.",
        "description": "`PUT /api/v1/organizations/active` — move this person to another organisation they belong to.\n\nExactly what the dashboard's switcher does: it writes `user.last_organization_id`, which is\nwhat every request on both stacks resolves the membership from. So it moves **every** session\nthis person holds, the web one included, and the next request anywhere lands in the new\norganisation. Nothing about the session itself is rewritten; better-auth's stored session\ncarries no organisation for this side to change.\n\n**Session only**, and no permission check: it acts on the pointer that decides which\norganisation a permission is checked in. The authorisation is the membership, confirmed in the\nsame statement that stores the pointer — an organisation this person is not in answers 404,\nthe same as one that does not exist. No audit entry: nothing in either organisation changed,\nand recording somebody's navigation in a log their colleagues read is not what it is for.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.SetActiveOrganizationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveOrganizationResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveOrganizationResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/probes": {
      "get": {
        "operationId": "probes",
        "tags": [
          "probes"
        ],
        "summary": "The organisation's own private probes, by name.",
        "description": "`GET /api/v1/probes` — the organisation's own private probes, by name.\n\nWhat the monitor form's \"check from my probe\" picker offers. Needs `agent:read`, the\npermission the dashboard's probes page is shown on. Organisation-scoped and not narrowed by\nworkspace, because a probe is a machine on the customer's network rather than something that\nbelongs to one client.\n\n**No region code, on any row.** Where a check runs is ours to decide and to change, and the\ncode behind a private probe is plumbing a customer cannot act on. Revoked probes are listed\nwith `revoked: true` and must not be offered: they can no longer lease anything.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListProbesResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListProbesResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "search",
        "tags": [
          "search"
        ],
        "summary": "Everything in the account matching `?q=`, whatever kind of thing it is.",
        "description": "`GET /api/v1/search` — everything in the account matching `?q=`, whatever kind of thing it is.\n\nMonitors, incidents and their postmortems, hosts, status pages, workspaces, notification\nchannels, maintenance windows, error projects and issues, analytics sites, members and\ninvitations, API keys, agents and private probes. One request rather than one per kind: this\nis what a search box calls on a keystroke.\n\n`?type=` narrows it to a comma-separated list of kinds — `monitor`, `incident`, `postmortem`,\n`host`, `status_page`, `workspace`, `channel`, `maintenance`, `error_project`, `error_issue`,\n`analytics_site`, `member`, `invitation`, `api_key`, `agent`, `probe`. `?limit=` caps how many\nof **each** kind come back, between 1 and 20, and defaults to 5.\n\nA term shorter than two characters answers with an empty list rather than a refusal: it means\n\"keep typing\", and a search box that reported an error on the first letter would read as\nbroken.\n\nEach kind is gated on the same permission its own collection endpoint uses, so search is\nnever a way round one. A kind this caller may not read is **absent rather than refused**, and\nabsent in exactly the way a kind with no matches is — the same rule the rest of the API\nfollows, so a response cannot be used to work out what a key is missing or which ids exist.\n\nNothing sealed, hashed or secret is matched against or returned: not a monitor's config, not\na channel's, not an API key's hash, not a heartbeat token. An API key is matched on its name\nand its displayable prefix, which exists to be shown.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/setup": {
      "get": {
        "operationId": "setup",
        "tags": [
          "setup"
        ],
        "summary": "The onboarding checklist, computed rather than stored.",
        "description": "`GET /api/v1/setup` — the onboarding checklist, computed rather than stored.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetSetupResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetSetupResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "dismiss_setup",
        "tags": [
          "setup"
        ],
        "summary": "Skip the checklist.",
        "description": "`DELETE /api/v1/setup` — skip the checklist.\n\n`org:update`, because dismissal is one organisation-wide row rather than a per-person\npreference: the checklist is about how the account is set up, and a colleague who arrives\nafter it was skipped should not be shown it again.\n\nRate limit: 30 writes a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DismissedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/DismissedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sms/enrolment": {
      "post": {
        "operationId": "start_sms_enrolment",
        "tags": [
          "sms"
        ],
        "summary": "Text a code to the caller's own mobile number.",
        "description": "`POST /api/v1/sms/enrolment` — text a code to the caller's own mobile number.\n\nThe web's personal SMS enrolment, one rule for one. **Session only**: a number is added by the\nperson holding the handset, against their own session, and confirmed with a code sent there —\nthat is the opt-in record, and a key in a script is not that person. Needs\n`notification_channel:manage`, as on the web.\n\nRefused before anything is stored when the plan has no SMS allowance at all (402,\n`sms_not_included`), when the input is not a number (`not_a_phone_number`), when no provider\ncarries that country for our sender (`sms_country_unsupported`), and when the route has to be\nopened for us first (`sms_route_not_open`, a request rather than a dead end). US and Canadian\nnumbers are refused, measured rather than assumed.\n\nOtherwise the number is stored **unconfirmed** — replacing any earlier SMS number of this\nmember — and the code is sent. The row is written first, deliberately: a failed send leaves a\nchannel awaiting confirmation with the provider's words on it (502,\n`verification_code_not_sent`), and calling again sends a new code. Unconfirmed receives\nnothing. `caveat` says what to tell somebody before they rely on it.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.StartSmsEnrolmentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SmsEnrolmentResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/SmsEnrolmentResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sms/enrolment/confirm": {
      "post": {
        "operationId": "confirm_sms_enrolment",
        "tags": [
          "sms"
        ],
        "summary": "Finish enrolment with the code that arrived.",
        "description": "`POST /api/v1/sms/enrolment/confirm` — finish enrolment with the code that arrived.\n\n**Session only** and `notification_channel:manage`, like starting it. The code is matched\nagainst the caller's own pending SMS channel and nobody else's, so guessing here is guessing\nat one's own number. One answer, `verification_code_incorrect`, for a wrong code, a used one\nand nothing pending: a separate \"nothing pending\" would still be a fact about account state.\nThe consent record is written to the audit log in the same transaction as the consent.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.ConfirmSmsEnrolmentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SmsConfirmedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/SmsConfirmedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sourcemaps": {
      "post": {
        "operationId": "upload_sourcemap",
        "tags": [
          "sourcemaps"
        ],
        "summary": "Upload a source map.",
        "description": "`POST /api/v1/sourcemaps` — upload a source map.\n\nDeliberately one request with a raw body rather than Sentry's chunked artifact-bundle\nprotocol. That protocol exists to make `sentry-cli` fast against very large uploads and it is\na lot of surface; this is a `curl` in a build script, which is what somebody actually needs at\nthe point they discover their stack traces are unreadable.\n\nThe map may be sent as `map` — an object — or gzipped as base64 in `mapGzipBase64`, which is\nwhat a script uploading a five-megabyte map should do.\n\n`monitor:create` is the wrong name and the right permission. There is no error-project action\nin `crates/core/src/rbac.rs` yet, and inventing one in a route would put a permission\nsomewhere roles are not defined. This is a configuration write by somebody who can already\nconfigure the account; when an error-project action is added, change this line and the symbols\none together.\n\nRate limit: 300 source maps an hour, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UploadSourceMapRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadedSourceMapResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/UploadedSourceMapResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 300 source maps an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/status-pages": {
      "get": {
        "operationId": "status_pages",
        "tags": [
          "status-pages"
        ],
        "summary": "The pages this account publishes.",
        "description": "`GET /api/v1/status-pages` — the pages this account publishes.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListStatusPagesResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListStatusPagesResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_status_page",
        "tags": [
          "status-pages"
        ],
        "summary": "Create one.",
        "description": "`POST /api/v1/status-pages` — create one.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateStatusPageRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/status-pages/{id}": {
      "get": {
        "operationId": "status_page",
        "tags": [
          "status-pages"
        ],
        "summary": "One page, with its components and groups.",
        "description": "`GET /api/v1/status-pages/{id}` — one page, with its components and groups.\n\nRate limit: 120 requests a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetStatusPageResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetStatusPageResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patch_status_page",
        "tags": [
          "status-pages"
        ],
        "summary": "Change one.",
        "description": "`PATCH /api/v1/status-pages/{id}` — change one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateStatusPageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedStatusPageResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedStatusPageResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "delete_status_page",
        "tags": [
          "status-pages"
        ],
        "summary": "Retire one.",
        "description": "`DELETE /api/v1/status-pages/{id}` — retire one.\n\nSoft: the row stays with `deleted_at` set, so the slug is not immediately reusable by\nsomebody else and an accidental deletion is recoverable by us. The page stops serving.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/status-pages/{id}/components": {
      "post": {
        "operationId": "add_component",
        "tags": [
          "status-pages"
        ],
        "summary": "Publish a monitor on a page.",
        "description": "`POST /api/v1/status-pages/{id}/components` — publish a monitor on a page.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.AddComponentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentAckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentAckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "remove_component",
        "tags": [
          "status-pages"
        ],
        "summary": "Unpublish one.",
        "description": "`DELETE /api/v1/status-pages/{id}/components` — unpublish one.\n\nBy component id — `?component=`, or `{ \"componentId\": \"...\" }`. The component rather than\nthe monitor because it is the row that carries the public display name, and it is what\n`GET /api/v1/status-pages/{id}` returns.\n\nA query parameter as well as a body because DELETE with a body is refused or dropped by\nenough HTTP clients that relying on it would make the endpoint unreachable from a caller\nwith no way to work around it.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "component",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.RemoveComponentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentAckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ComponentAckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/summary": {
      "get": {
        "operationId": "summary",
        "tags": [
          "summary"
        ],
        "summary": "Counts, for a wallboard or a daily digest.",
        "description": "`GET /api/v1/summary` — counts, for a wallboard or a daily digest.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetSummaryResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/GetSummaryResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/symbols": {
      "post": {
        "operationId": "upload_symbols",
        "tags": [
          "symbols"
        ],
        "summary": "Upload an iOS dSYM or an Android R8 mapping.",
        "description": "`POST /api/v1/symbols` — upload an iOS dSYM or an Android R8 mapping.\n\n## Why the metadata is in headers and the body is raw bytes\n\nA dSYM is tens of megabytes. Base64 in a JSON field, which is how the source-map route accepts\na large map, costs a third again in transfer and forces the whole thing through a string\nbefore it can be written — for an artefact an order of magnitude larger than the biggest map.\nThe body is the gzipped bytes and nothing else.\n\n**This route does not negotiate a request encoding**, and that is not an omission: there is no\nrequest body to decode. The reply does negotiate, like everything else.\n\n## Resolution happens when an issue is read, never here\n\nEvery reason `CLAUDE.md` gives for that on source maps applies harder. Ingest is the hot path\nwith the weakest authentication, so decompressing and indexing DWARF there is work an attacker\nwould like to cause; and symbols uploaded *after* the crashes arrived still help, which is the\nusual order because nobody uploads symbols until they need them.\n\nRate limit: 20 symbol uploads an hour, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadedSymbolsResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/UploadedSymbolsResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 20 symbol uploads an hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workspaces": {
      "get": {
        "operationId": "workspaces",
        "tags": [
          "workspaces"
        ],
        "summary": "The workspaces this caller may write into.",
        "description": "`GET /api/v1/workspaces` — the workspaces this caller may write into.\n\nA mobile client needs these before it can create anything: `POST /api/v1/monitors` requires\na `workspaceId` and deliberately refuses to guess one.\n\nRate limit: 120 requests a minute, per organisation.",
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListWorkspacesResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/ListWorkspacesResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 120 requests a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "create_workspace",
        "tags": [
          "workspaces"
        ],
        "summary": "Create a client workspace.",
        "description": "`POST /api/v1/workspaces` — create a client workspace.\n\nRate limit: 30 writes a minute, per organisation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.CreateWorkspaceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workspaces/{id}": {
      "patch": {
        "operationId": "patch_workspace",
        "tags": [
          "workspaces"
        ],
        "summary": "Rename a workspace, or change its client reference.",
        "description": "`PATCH /api/v1/workspaces/{id}` — rename a workspace, or change its client reference.\n\nNeeds `workspace:update`, checked against the workspace itself after it is resolved under this\norganisation — an id from another account is \"Not found.\" rather than a rename of a stranger's\nworkspace, including for an unconfined admin who would otherwise match every workspace id.\n\n`name` is required, 1 to 80 characters after trimming. `clientReference` is optional: absent\nleaves it, `null` clears it. **The slug never changes** — it is in status-page URLs a client\nmay have bookmarked. Answers with the workspace as it now reads.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/requests.UpdateWorkspaceRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedWorkspaceResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatedWorkspaceResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "delete_workspace",
        "tags": [
          "workspaces"
        ],
        "summary": "Retire one.",
        "description": "`DELETE /api/v1/workspaces/{id}` — retire one.\n\nRate limit: 30 writes a minute, per organisation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              },
              "application/x-protobuf": {
                "schema": {
                  "$ref": "#/components/schemas/AckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was malformed, or a field was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's role does not allow this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such row — or it belongs to another organisation, which answers the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit: 30 writes a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from `/settings/api-keys`, sent as `Authorization: Bearer …`."
      }
    },
    "schemas": {
      "Summary": {
        "type": "object",
        "description": "The counts behind the dashboard's header.\n\nEvery figure here is computed in SQL rather than by counting a list, which is the point —\nand `summary_matches_the_list` in the integration tests is what keeps the two honest.",
        "properties": {
          "total": {
            "type": "integer",
            "format": "int64"
          },
          "up": {
            "type": "integer",
            "format": "int64"
          },
          "degraded": {
            "type": "integer",
            "format": "int64"
          },
          "down": {
            "type": "integer",
            "format": "int64"
          },
          "paused": {
            "type": "integer",
            "format": "int64"
          },
          "pending": {
            "type": "integer",
            "format": "int64"
          },
          "openIncidents": {
            "type": "integer",
            "format": "int64"
          },
          "suppressedIncidents": {
            "type": "integer",
            "format": "int64",
            "description": "Incidents held back because they are blast radius of another one.\n\nCounted apart rather than hidden: they are real, and they are not separate outages."
          }
        },
        "required": [
          "total",
          "up",
          "degraded",
          "down",
          "paused",
          "pending",
          "openIncidents",
          "suppressedIncidents"
        ]
      },
      "GetSummaryResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Summary"
          }
        },
        "required": [
          "data"
        ]
      },
      "SetupProgress": {
        "type": "object",
        "description": "The onboarding checklist, which stores no ticks.",
        "properties": {
          "hasMonitor": {
            "type": "boolean"
          },
          "hasConfirmedChannel": {
            "type": "boolean",
            "description": "Whether an alert could actually **arrive**, not whether a channel exists.\n\nThose came apart badly once: every account created between two migrations had a channel on\nits own address that the notifier refused. A checklist that ticked \"alerts are set up\" for\na channel that cannot send would repeat that bug as reassurance."
          },
          "hasStatusPage": {
            "type": "boolean"
          },
          "hasColleague": {
            "type": "boolean",
            "description": "Memberships, never invitations. An invitation sent and not accepted has added nobody, and\nticking it would say a job was finished when the person it was for never arrived."
          },
          "dismissedAt": {
            "type": "string",
            "description": "Absent while the checklist is still showing."
          }
        },
        "required": [
          "hasMonitor",
          "hasConfirmedChannel",
          "hasStatusPage",
          "hasColleague"
        ]
      },
      "GetSetupResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SetupProgress"
          }
        },
        "required": [
          "data"
        ]
      },
      "AuditEntry": {
        "type": "object",
        "description": "One entry in the audit log.",
        "properties": {
          "id": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "actor": {
            "$ref": "#/components/schemas/AuditActor"
          },
          "action": {
            "type": "string"
          },
          "resource": {
            "$ref": "#/components/schemas/AuditResource"
          },
          "workspace": {
            "$ref": "#/components/schemas/AuditWorkspace"
          },
          "changes": {
            "type": "string",
            "description": "Before and after for the fields that changed, as a JSON object.\n\n**Never a credential, a hash or a secret.** The writers redact at the point of writing\nrather than here, because an audit log holding credentials is a credential store with\nworse access control than the one it is auditing, whatever a reader chooses to print."
          },
          "ipAddress": {
            "type": "string"
          },
          "client": {
            "type": "string",
            "description": "What the change was made from: `dashboard`, `ios`, `android`, `api_key`, `mcp` or `browser`.\nAbsent for an entry written before this was recorded, or by a recipient following a link in\na message with no session at all. `ios` and `android` are what the app said it was; the\nothers follow from how the request was authenticated."
          },
          "userAgent": {
            "type": "string",
            "description": "The user agent the change was made with, cut to 256 characters — the detail behind `client`."
          }
        },
        "required": [
          "id",
          "createdAt",
          "actor",
          "action",
          "resource"
        ]
      },
      "AuditActor": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "The address or name recorded at the time.\n\n**Never absent.** It is written as a snapshot precisely so it survives the actor being\ndeleted."
          },
          "userId": {
            "type": "string"
          },
          "apiKeyId": {
            "type": "string"
          }
        },
        "required": [
          "label"
        ]
      },
      "AuditResource": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Named `type` in JSON, which is a reserved word in more languages than it is not — hence\nthe field name here and the rename on the way out."
          },
          "id": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ]
      },
      "AuditWorkspace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ]
      },
      "ListAuditResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditEntry"
            }
          },
          "nextBefore": {
            "type": "string",
            "description": "The cursor for the next page, taken from the last entry actually returned — so it is a\nvalue that exists in the log rather than a computed timestamp that might fall between two\nrows written in the same millisecond.\n\nAbsent on the last page, and present only when there is genuinely something behind it."
          }
        }
      },
      "Dismissed": {
        "type": "object",
        "description": "The answer to skipping the onboarding checklist.\n\nA field rather than an empty body, because a client that got `{}` would have to decide what it\nmeant. There is no id here: the row it touched is the organisation, which the caller is.",
        "properties": {
          "dismissed": {
            "type": "boolean"
          }
        },
        "required": [
          "dismissed"
        ]
      },
      "DismissedResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Dismissed"
          }
        },
        "required": [
          "data"
        ]
      },
      "AccountDeleted": {
        "type": "object",
        "description": "What deleting your own account took with it.\n\nCounted rather than listed: a client shows a confirmation screen, and the ids are of rows that\nno longer exist.",
        "properties": {
          "deleted": {
            "type": "boolean"
          },
          "removed": {
            "$ref": "#/components/schemas/DeletedCounts"
          }
        },
        "required": [
          "deleted",
          "removed"
        ]
      },
      "DeletedCounts": {
        "type": "object",
        "properties": {
          "organizations": {
            "type": "integer",
            "format": "int64",
            "description": "Organisations this person was a member of. The organisations themselves are **not** deleted\n— one with no members is unreachable rather than gone, which is the safer of the two\nmistakes: deleting it would take its incidents, its monitors and its audit history with it."
          },
          "personalChannels": {
            "type": "integer",
            "format": "int64",
            "description": "Their own alert destinations. A shared channel — a #alerts webhook, a distribution list —\nhas no owner and survives, because it belongs to the team."
          },
          "apiKeys": {
            "type": "integer",
            "format": "int64"
          },
          "sessions": {
            "type": "integer",
            "format": "int64",
            "description": "Every session, not just this one: signing the phone out and leaving the browser signed in\nwould mean the account they just deleted is still open somewhere."
          }
        },
        "required": [
          "organizations",
          "personalChannels",
          "apiKeys",
          "sessions"
        ]
      },
      "AccountDeletedResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AccountDeleted"
          }
        },
        "required": [
          "data"
        ]
      },
      "Agent": {
        "type": "object",
        "description": "A server agent, as the listing returns it.\n\n**No `ip_address`.** It is recorded for our own support questions and is the customer's\ninfrastructure, not something a listing needs to publish — and a column that arrives in every\nrow is one somebody eventually prints.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "hostname": {
            "type": "string"
          },
          "os": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "lastSeenAt": {
            "type": "string",
            "description": "The one field that matters operationally. An agent that has not reported is either\nuninstalled, firewalled or on a dead host, and none of those is visible any other way —\nthe token hash says nothing and the enrolment date says less."
          },
          "enrolledAt": {
            "type": "string"
          },
          "host": {
            "allOf": [
              {
                "$ref": "#/components/schemas/HostLink"
              }
            ],
            "description": "The machine somebody said this agent runs on, or **null** — which is the ordinary state.\n\nNever inferred from `hostname`: that is whatever the machine calls itself, routinely not\nwhat the customer calls the box in their fleet, and a guess that is right most of the time\nwould attribute one machine's disk usage to another."
          }
        },
        "required": [
          "id",
          "name",
          "workspace"
        ]
      },
      "ListAgentsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Agent"
            }
          }
        }
      },
      "AgentMetric": {
        "type": "object",
        "description": "One sample an agent posted.\n\nEvery gauge is optional because an agent reports what its platform can measure: a container\nwith no swap sends no swap figure, and a zero there would be a claim rather than a gap.",
        "properties": {
          "createdAt": {
            "type": "string"
          },
          "cpuPercent": {
            "type": "number",
            "format": "float"
          },
          "memoryPercent": {
            "type": "number",
            "format": "float"
          },
          "memoryUsedBytes": {
            "type": "number",
            "format": "double",
            "description": "`double precision`, not an integer. A byte count above 2^53 is not a real machine, but the\nagent reports these as JSON numbers and the column stores what it sent — narrowing here\nwould round a value the customer's own agent produced."
          },
          "swapPercent": {
            "type": "number",
            "format": "float"
          },
          "diskPercent": {
            "type": "number",
            "format": "float"
          },
          "disks": {
            "type": "string",
            "description": "Per-filesystem figures, as the agent sent them. A JSON array carried in a string: the shape\nis the agent's, and an agent newer than this server must not have fields dropped."
          },
          "netRxBytes": {
            "type": "number",
            "format": "double"
          },
          "netTxBytes": {
            "type": "number",
            "format": "double"
          },
          "loadAvg1": {
            "type": "number",
            "format": "float"
          },
          "loadAvg5": {
            "type": "number",
            "format": "float"
          },
          "loadAvg15": {
            "type": "number",
            "format": "float"
          },
          "uptimeSeconds": {
            "type": "number",
            "format": "double"
          },
          "services": {
            "type": "string",
            "description": "Per-service figures, same reasoning as `disks`."
          }
        },
        "required": [
          "createdAt"
        ]
      },
      "AgentMetrics": {
        "type": "object",
        "description": "An agent's recent samples, oldest first.\n\nAn **empty** list is a live agent that has not reported yet, which is a real and different\nstate from \"no such agent\" — that one is a 404, so a client can tell them apart.",
        "properties": {
          "agentId": {
            "type": "string"
          },
          "metrics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentMetric"
            }
          }
        },
        "required": [
          "agentId"
        ]
      },
      "GetAgentMetricsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AgentMetrics"
          }
        },
        "required": [
          "data"
        ]
      },
      "PrivateProbe": {
        "type": "object",
        "description": "One of the organisation's own private probes, for choosing where a monitor runs.\n\n**Never its region code.** A customer picks \"check from my probe\", by name; the code behind it\nis our plumbing, and a field that arrives in every row is one somebody eventually renders.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "lastSeenAt": {
            "type": "string",
            "description": "Absent on a probe that has never leased work. That is usually one whose container never\nstarted, which is the thing worth telling somebody."
          },
          "version": {
            "type": "string"
          },
          "hostname": {
            "type": "string",
            "description": "The machine name the probe reports. The customer's own, so it is safe to return."
          },
          "monitorCount": {
            "type": "integer",
            "format": "int32",
            "description": "Live monitors pinned to this probe."
          },
          "revoked": {
            "type": "boolean",
            "description": "A revoked probe is listed so \"had one\" reads differently from \"never had one\", and must never\nbe offered in a picker: it can no longer lease anything."
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "monitorCount",
          "revoked",
          "createdAt"
        ]
      },
      "ListProbesResponse": {
        "type": "object",
        "description": "`GET /api/v1/probes`",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PrivateProbe"
            }
          }
        }
      },
      "AnalyticsSite": {
        "type": "object",
        "description": "A site this account measures.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "enabled": {
            "type": "boolean"
          },
          "publicId": {
            "type": "string",
            "description": "The `data-site` value, public by design — it is in the page source of every page the site\nserves.\n\nThe **share token** for a published dashboard is deliberately not here: it is derived from\nthe site id, so anything holding it can read the traffic figures without a key, and a\nlisting endpoint is not where a capability like that should leak out of."
          },
          "viewsLast24h": {
            "type": "integer",
            "format": "int32"
          },
          "trafficAlerting": {
            "type": "boolean",
            "description": "Whether the traffic alert is currently firing. The most useful single field here, and the\nreason somebody would poll this rather than the per-site endpoint."
          }
        },
        "required": [
          "id",
          "name",
          "domain",
          "workspace",
          "enabled",
          "publicId",
          "viewsLast24h",
          "trafficAlerting"
        ]
      },
      "ListAnalyticsSitesResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsSite"
            }
          }
        }
      },
      "AnalyticsPoint": {
        "type": "object",
        "description": "One point of the series. The grain switches at two days: hourly below, daily above.",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "pageviews": {
            "type": "integer",
            "format": "int32"
          },
          "visitors": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "bucket",
          "pageviews",
          "visitors"
        ]
      },
      "AnalyticsRange": {
        "type": "object",
        "properties": {
          "days": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "days"
        ]
      },
      "AnalyticsSiteDetail": {
        "type": "object",
        "description": "A site's figures over a window.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "range": {
            "$ref": "#/components/schemas/AnalyticsRange"
          },
          "pageviews": {
            "type": "integer",
            "format": "int32"
          },
          "dailyUniqueVisitors": {
            "type": "integer",
            "format": "int32",
            "description": "Named so the caller has to notice what it is.\n\nIt is the sum of daily unique visitors and cannot be anything else: the salt behind a\nvisitor hash is destroyed at the end of its day, so somebody who came twice is two and\nthere is no key that would join them. A field called `visitors` next to a thirty-day range\ninvites the reader to assume otherwise, and an integrator who assumes is putting a wrong\nnumber in front of somebody."
          },
          "events": {
            "type": "integer",
            "format": "int32"
          },
          "singlePagePercent": {
            "type": "integer",
            "format": "int64",
            "description": "Visits that saw exactly one page, as a percentage. Null when there were no visitors."
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsPoint"
            }
          },
          "dimensions": {
            "type": "string",
            "description": "The breakdown tables, keyed by dimension name, as a JSON object of arrays.\n\n**Absent unless asked for**, rather than an empty object: a client can tell \"I did not ask\"\nfrom \"I asked and there is nothing\". That is the one field in this API whose absence is\nload-bearing rather than incidental, which is why it is a string of JSON — the keys are\nwhichever dimensions the caller named, in the order they named them, and a protobuf map\nwould reorder them and a repeated field would change the shape."
          }
        },
        "required": [
          "id",
          "name",
          "domain",
          "range",
          "pageviews",
          "dailyUniqueVisitors",
          "events"
        ]
      },
      "GetAnalyticsSiteResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AnalyticsSiteDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "BillingDetail": {
        "type": "object",
        "description": "What the organisation is paying, and how.\n\nThe only endpoint that answers \"where does this subscription live\", which decides what a\nclient's plan screen may offer: an Apple subscriber is sent to their App Store subscription\nsettings, a Paddle subscriber to the web, and neither is ever shown a buy button for the\nother. Apple permits telling somebody their account is already subscribed; it does not permit\nlinking out to buy.\n\n**No Paddle ids.** `paddle_subscription_id` and `paddle_customer_id` are credentials for\nlooking things up in Paddle, and a client needs the provider and the date, not an id it cannot\nuse. They are left out of the query rather than out of this message, which is the version that\ncannot be printed by accident.",
        "properties": {
          "plan": {
            "type": "string"
          },
          "planName": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "`paddle`, `apple`, `manual` or `none`.\n\n`none` is a free plan with nothing to renew; `manual` is a paid plan somebody applied by\nhand — a comped account, a migration, an invoice deal. This used to be inferred from the\n*absence* of a subscription row, which was right only while Paddle was the sole source: an\nApple subscriber has a row, and would have been told there was nothing to renew and no card\non file."
          },
          "status": {
            "type": "string",
            "description": "Null on a free plan, and on a manual one: there is no subscription row behind either."
          },
          "cadence": {
            "type": "string"
          },
          "currentPeriodEnd": {
            "type": "string"
          },
          "trialEndsAt": {
            "type": "string"
          },
          "cancelAt": {
            "type": "string"
          },
          "addOnPacks": {
            "type": "string",
            "description": "What they are currently paying for, as `{\"monitors_50\": 2}`. An empty object where there is\nnothing."
          },
          "appleProductId": {
            "type": "string"
          },
          "canPurchaseInApp": {
            "type": "boolean",
            "description": "What the client may offer, decided here rather than by the client re-deriving it from\n`source`. Two clients deriving the same rule is how one of them ends up offering an in-app\npurchase to somebody already paying by card — which the server then refuses on\n`subscriptions_one_live_per_org_idx`, *after* Apple has taken the money."
          },
          "canCheckoutOnWeb": {
            "type": "boolean"
          },
          "invoices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            },
            "description": "The last twelve, not all of them. A receipts list is a scroll on a phone, and the full\nhistory is on the web billing page."
          }
        },
        "required": [
          "plan",
          "planName",
          "source",
          "addOnPacks",
          "canPurchaseInApp",
          "canCheckoutOnWeb"
        ]
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "total": {
            "type": "integer",
            "format": "int64",
            "description": "Minor units — cents, tetri. `bigint` in the schema: a total in minor units on an annual\ninvoice in a weak currency does not fit comfortably in 32 bits."
          },
          "tax": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "billedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "total",
          "tax",
          "currency",
          "status"
        ]
      },
      "GetBillingResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/BillingDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "AppleVerification": {
        "type": "object",
        "description": "What `POST /api/v1/billing/apple/verify` made of a StoreKit transaction.",
        "properties": {
          "plan": {
            "type": "string"
          },
          "cadence": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string"
          },
          "cancelAt": {
            "type": "string"
          },
          "environment": {
            "type": "string",
            "description": "`Production` or `Sandbox`, as **Apple** answered — not as this deployment is built. A\nTestFlight purchase is a sandbox one against a production server, and storing the wrong one\nsends the next lookup to the wrong host."
          },
          "applied": {
            "type": "boolean",
            "description": "False means a newer event had already been applied — a renewal notification that beat the\napp's own call. The entitlement is correct either way, and saying so stops the app treating\nit as a failure and retrying in a loop."
          }
        },
        "required": [
          "plan",
          "cadence",
          "status",
          "environment",
          "applied"
        ]
      },
      "AppleVerifyResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AppleVerification"
          }
        },
        "required": [
          "data"
        ]
      },
      "Error": {
        "type": "object",
        "description": "The refusal envelope, on every 4xx and 5xx this API produces.\n\n`code` exists because `error` is prose for a human and may be reworded or translated at any\ntime, while `code` is what a client may branch on. Without it the mobile app could not tell\n\"there is no session\" from \"your role may not do that\" — both arrive as a 4xx — and so it\nmatched on the text of the sentence.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Prose. Do not branch on it."
          },
          "code": {
            "type": "string",
            "description": "Stable. This is the one to branch on."
          },
          "vars": {
            "type": "string",
            "description": "The values the sentence names, as a JSON object of strings — absent when it names none.\n\n`error` interpolates them already; these are here so a client can render the refusal in its\nown language. A translation has to be free to put a number somewhere else in the sentence,\nwhich it cannot do if the only copy of that number is inside an English one.\n\n`{\"field\": \"name\"}` beside `missing_field`, `{\"limit\": \"3\", \"current\": \"3\", \"plan\": \"Pro\",\n\"resource\": \"statusPages\"}` beside `limit_reached`. Strings rather than a mixed-type map:\nevery one of them is going into a sentence, and a client that formats a number for its own\nlocale wants the digits, not a float."
          }
        },
        "required": [
          "error"
        ]
      },
      "Workspace": {
        "type": "object",
        "description": "A workspace, as it appears embedded in something that belongs to one.\n\nAn agency account has several; everything a confined member can see is narrowed to theirs,\nwhich is why the id travels with every row rather than being looked up.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "StringList": {
        "type": "object",
        "description": "A list of ids that can also be **null**, which is a different answer from empty.\n\nProtobuf has no nullable repeated field, and the distinction is load-bearing in three places:\na member's `workspaceIds` is null for the whole organisation and empty for nothing at all; an\nAPI key's `scopes` is null for \"everything the owner's role allows\"; and a client that read\nthe two the same way would show a confined member everything.\n\nWrapping it in a message is the only way to carry both. Its JSON is still the **bare array**,\nnever `{\"values\": [...]}` — the API has always served an array or a null, and a wrapper object\nappearing in the middle of a body is not something a client recovers from.",
        "properties": {
          "values": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ErrorProject": {
        "type": "object",
        "description": "An error-tracking project.\n\nRead-only, deliberately: creating one hands back a `public_key` that still has to be pasted\ninto a DSN in somebody's application config, so automating it does the easy half of a two-step\njob. What is worth an endpoint is seeing what is broken.",
        "properties": {
          "id": {
            "type": "string"
          },
          "ref": {
            "type": "integer",
            "format": "int64",
            "description": "The number in the DSN. What somebody matches against their own config.\n\n`bigint`, not `int` — it comes from a sequence shared across every project in the product,\nso it is not bounded by any one account's project count."
          },
          "name": {
            "type": "string"
          },
          "platform": {
            "type": "string"
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "publicKey": {
            "type": "string",
            "description": "Public by construction — it is in the DSN inside every copy of the customer's own\napplication, which is the whole point of a Sentry public key. It authorises writing events\nto this project and reading nothing."
          },
          "alertOnNewIssue": {
            "type": "boolean"
          },
          "unresolved": {
            "type": "integer",
            "format": "int32"
          },
          "eventsLast24h": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "id",
          "ref",
          "name",
          "workspace",
          "publicKey",
          "alertOnNewIssue",
          "unresolved",
          "eventsLast24h"
        ]
      },
      "ListErrorProjectsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ErrorProject"
            }
          }
        }
      },
      "Issue": {
        "type": "object",
        "description": "A group of errors sharing a fingerprint.\n\nGrouping never includes line numbers, so adding an import at the top of a file does not split\none issue into two.",
        "properties": {
          "id": {
            "type": "string"
          },
          "project": {
            "allOf": [
              {
                "$ref": "#/components/schemas/IssueProject"
              }
            ],
            "description": "Which project, so a caller listing across all of them can say where each came from without\na second request per row."
          },
          "type": {
            "type": "string",
            "description": "The exception class, e.g. `TypeError`. Named `type` in JSON, which is a reserved word in\nmore languages than it is not."
          },
          "value": {
            "type": "string"
          },
          "culprit": {
            "type": "string",
            "description": "Where it was thrown, as the SDK saw it. Often the most useful line in the row."
          },
          "level": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "timesSeen": {
            "type": "integer",
            "format": "int64",
            "description": "`bigint`: one fingerprint can absorb a very large number of occurrences, which is the\nentire point of grouping them."
          },
          "firstSeen": {
            "type": "string"
          },
          "lastSeen": {
            "type": "string"
          },
          "lastRelease": {
            "type": "string"
          },
          "spark": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Twenty-four hourly counts. Says \"this started an hour ago\" in a way a `last_seen` timestamp\ndoes not."
          }
        },
        "required": [
          "id",
          "project",
          "type",
          "value",
          "level",
          "status",
          "timesSeen",
          "firstSeen",
          "lastSeen"
        ]
      },
      "IssueProject": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ListIssuesResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Issue"
            }
          }
        }
      },
      "UploadedSourceMap": {
        "type": "object",
        "description": "A source map that is now stored.",
        "properties": {
          "id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "debugId": {
            "type": "string"
          },
          "release": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int32",
            "description": "Compressed size, which is what the quota is measured in."
          },
          "matching": {
            "type": "string",
            "description": "How a frame will find this map: `debug id`, `release and filename`, or `filename only`.\n\nSaid plainly, because a map uploaded without a debug id and without a release can only be\nmatched by filename and will silently fail to apply if the file is hashed — which is the\nordinary case for a production bundle."
          }
        },
        "required": [
          "id",
          "filename",
          "sizeBytes",
          "matching"
        ]
      },
      "UploadedSourceMapResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UploadedSourceMap"
          }
        },
        "required": [
          "data"
        ]
      },
      "UploadedSymbols": {
        "type": "object",
        "description": "Debug symbols that are now stored.",
        "properties": {
          "uploaded": {
            "type": "integer",
            "format": "int32",
            "description": "One row per debug id: one dSYM covers one architecture of one binary, and a fat build\nproduces several."
          },
          "platform": {
            "type": "string"
          },
          "release": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "artifacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SymbolArtifact"
            }
          }
        },
        "required": [
          "uploaded",
          "platform",
          "sizeBytes"
        ]
      },
      "SymbolArtifact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "debugId": {
            "type": "string",
            "description": "Absent for an R8 mapping, which has no id at all — the release string is its only key."
          }
        },
        "required": [
          "id"
        ]
      },
      "UploadedSymbolsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UploadedSymbols"
          }
        },
        "required": [
          "data"
        ]
      },
      "IssueDetail": {
        "type": "object",
        "description": "One issue: what threw, where, and the stack as the customer's own source.",
        "properties": {
          "id": {
            "type": "string"
          },
          "project": {
            "$ref": "#/components/schemas/IssueDetailProject"
          },
          "type": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "culprit": {
            "type": "string"
          },
          "level": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "timesSeen": {
            "type": "integer",
            "format": "int64"
          },
          "firstSeen": {
            "type": "string"
          },
          "lastSeen": {
            "type": "string"
          },
          "firstRelease": {
            "type": "string"
          },
          "lastRelease": {
            "type": "string"
          },
          "resolvedInRelease": {
            "type": "string",
            "description": "The release an issue was resolved in, so a straggler from an older deployment does not\nreopen it. Null when the SDK sent no release, in which case any later event does reopen it —\nwith no way to tell a straggler from a regression, the safe assumption is that the bug is\nback."
          },
          "exceptions": {
            "type": "string",
            "description": "The exception chain, as a JSON document, with its frames **resolved**.\n\nSentry orders a chain with the thrown error last and its causes before it, and it is\nreturned in that order rather than reversed, so a client reads the SDK's own convention.\n\nA document rather than a message: this is the SDK's own shape, it differs by platform and by\nSDK version, and a closed message would silently drop whatever a newer one sent — on the\nendpoint whose entire purpose is to show what actually happened. A frame that could not be\nresolved is present and minified rather than missing."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IssueTag"
            }
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IssuePoint"
            },
            "description": "Hourly counts over the last fortnight. `generate_series`-backed, so a quiet hour is a zero\nrather than a missing point."
          },
          "lastEvent": {
            "$ref": "#/components/schemas/LastEvent"
          }
        },
        "required": [
          "id",
          "project",
          "type",
          "value",
          "level",
          "status",
          "timesSeen",
          "firstSeen",
          "lastSeen",
          "exceptions"
        ]
      },
      "IssueDetailProject": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "ref": {
            "type": "integer",
            "format": "int64",
            "description": "The number in the DSN, which is what somebody matches against their own config."
          }
        },
        "required": [
          "id",
          "name",
          "ref"
        ]
      },
      "IssueTag": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "key",
          "value",
          "count"
        ]
      },
      "IssuePoint": {
        "type": "object",
        "properties": {
          "bucket": {
            "type": "string"
          },
          "events": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "bucket",
          "events"
        ]
      },
      "LastEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "occurredAt": {
            "type": "string",
            "description": "Two timestamps, because they are two facts. `occurredAt` is what the SDK reported and\n`receivedAt` is when ingest wrote it; a phone that was offline, or a queue that backed up, is\nexactly the gap between them — and collapsing the pair hides the one case where it matters."
          },
          "receivedAt": {
            "type": "string"
          },
          "release": {
            "type": "string"
          },
          "environment": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "receivedAt"
        ]
      },
      "GetIssueResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/IssueDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "IssueStatus": {
        "type": "object",
        "description": "The acknowledgement a status change returns.",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status"
        ]
      },
      "IssueStatusResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/IssueStatus"
          }
        },
        "required": [
          "data"
        ]
      },
      "Host": {
        "type": "object",
        "description": "A machine, and the second way monitors group.\n\nA workspace says whose a monitor is; a host says what it runs on. Both are optional to use\nand neither substitutes for the other: `host5.example.com` with thirty sites on it answers\n\"do I have to reboot something\", and the workspace answers \"whose client is affected\".\n\nHosts belong to the organisation and carry no workspace of their own, because one box\nroutinely holds several clients' sites. A confined key is therefore shown a host counted from\nits own monitors, and a host carrying none of them is not returned at all.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "description": "The agency's own notes about the machine: who it is rented from, what address it answers\non, anything. None of it is read by the product — a monitor's own configuration holds\nwhatever a probe connects to."
          },
          "address": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "The host's own status — `up`, `degraded`, `down`, `maintenance` — or **null**.\n\nNull is not `up`. It means nothing attributable has reported: the host has no monitors\npinned to it alone, or the ones it has are paused. A client that rendered a missing status\nas healthy would put a green tick on a machine nobody is watching."
          },
          "pinned": {
            "type": "integer",
            "format": "int32",
            "description": "Monitors on this host and on no other. These are what `status` is made of."
          },
          "spanning": {
            "type": "integer",
            "format": "int32",
            "description": "Monitors here that also run on another host — a load-balanced endpoint, a CDN, an\nautoscaling group. Counted and listed, **never rolled into `status`**: when one of them\nfails it says the service is broken and not which machine broke it, so attributing it to\neach host would mark every healthy node in the pool down at once."
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "pinned",
          "spanning",
          "createdAt"
        ]
      },
      "HostMonitor": {
        "type": "object",
        "description": "One monitor on a host, as the host's own response lists it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "pinned": {
            "type": "boolean",
            "description": "True when this monitor names this host and no other, which is what makes it count towards\nthe host's status."
          },
          "alsoOn": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The other hosts it answers from, by name. Empty for a pinned monitor.\n\nThis is the line somebody reads before rebooting a box: the pinned monitors go dark and\nthese probably do not."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "status",
          "workspace",
          "pinned"
        ]
      },
      "HostLink": {
        "type": "object",
        "description": "A host as it appears on a monitor — the id and the name, nothing derived.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ListHostsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Host"
            }
          }
        }
      },
      "HostDetail": {
        "type": "object",
        "description": "One host, with the monitors on it.\n\nBoth groups in one response rather than two endpoints: the whole point of the page is the\nsplit between them, and a client that fetched one would show a machine's health without the\nmonitors that do not decide it.",
        "properties": {
          "host": {
            "$ref": "#/components/schemas/Host"
          },
          "monitors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostMonitor"
            }
          }
        },
        "required": [
          "host"
        ]
      },
      "GetHostResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/HostDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "ListMonitorHostsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostLink"
            }
          }
        }
      },
      "Incident": {
        "type": "object",
        "description": "An incident, as the list returns it.\n\nThe list and the detail differ by one field, which is an accident of history rather than a\ndesign — and it is reproduced rather than tidied, because a field that appears in one and\nnot the other is exactly the kind of difference a client has already built around.",
        "properties": {
          "id": {
            "type": "string"
          },
          "monitorId": {
            "type": "string"
          },
          "monitorName": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "cause": {
            "type": "string",
            "description": "The one substantive line of the failure that opened it, in the organisation's language."
          },
          "status": {
            "type": "string",
            "description": "`open`, `acknowledged`, `suppressed` or `resolved`."
          },
          "severity": {
            "type": "string",
            "description": "`down` or `degraded`.\n\nA failure while another region is fine is `degraded`, not `down`: down should mean the\nsite is unreachable, not that one vantage point cannot see it."
          },
          "startedAt": {
            "type": "string"
          },
          "resolvedAt": {
            "type": "string"
          },
          "durationSeconds": {
            "type": "integer",
            "format": "int32"
          },
          "acknowledgedAt": {
            "type": "string"
          },
          "rootIncidentId": {
            "type": "string",
            "description": "Present when this incident is blast radius of another.\n\nA caller building an alert feed should skip these, which is only possible if we say so."
          },
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostLink"
            },
            "description": "The machines the failing monitor runs on, if any were recorded.\n\n**Empty is the ordinary case**: hosts are optional. One host answers the first question of\nan outage that a monitor name cannot — whether three incidents are three problems or one\nbox. More than one says the check answers from several machines, so it cannot say which of\nthem is at fault, and a client should not pick one."
          },
          "hasPostmortem": {
            "type": "boolean",
            "description": "Whether somebody has written a postmortem for it. The text itself is on the detail endpoint\nonly: a list of a hundred incidents should not carry a hundred documents."
          }
        },
        "required": [
          "id",
          "monitorId",
          "monitorName",
          "title",
          "status",
          "severity",
          "startedAt",
          "hasPostmortem"
        ]
      },
      "IncidentDetail": {
        "type": "object",
        "description": "The same incident with the workspace, which only the detail endpoint returns.",
        "properties": {
          "id": {
            "type": "string"
          },
          "monitorId": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          },
          "monitorName": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "cause": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "severity": {
            "type": "string"
          },
          "startedAt": {
            "type": "string"
          },
          "resolvedAt": {
            "type": "string"
          },
          "durationSeconds": {
            "type": "integer",
            "format": "int32"
          },
          "acknowledgedAt": {
            "type": "string"
          },
          "rootIncidentId": {
            "type": "string"
          },
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostLink"
            },
            "description": "The machines the failing monitor runs on, if any. Empty is the ordinary case — see\n`Incident`."
          },
          "postmortem": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Postmortem"
              }
            ],
            "description": "What happened, why, and what changes. Absent until somebody writes one, which is only\npossible once the incident is resolved.\n\n**Internal, never published.** It names people, systems and mistakes; no status page and\nno public endpoint reads it."
          }
        },
        "required": [
          "id",
          "monitorId",
          "workspaceId",
          "monitorName",
          "title",
          "status",
          "severity",
          "startedAt"
        ]
      },
      "Postmortem": {
        "type": "object",
        "description": "An incident's postmortem.",
        "properties": {
          "body": {
            "type": "string",
            "description": "Markdown, in the subset `packages/mobile-shared/src/markdown` defines. **Render it with that\nparser, never as HTML** — the subset has no raw HTML and a client that passed this to a web\nview would be the one place it did."
          },
          "authorName": {
            "type": "string",
            "description": "Whoever saved it last. Absent once that person's account is gone; the text stays."
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "body",
          "updatedAt"
        ]
      },
      "IncidentEvent": {
        "type": "object",
        "description": "One line of an incident's timeline.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "actor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/EventActor"
              }
            ],
            "description": "Absent for anything the system did.\n\nDeliberately not filled in with \"System\": that is a decision for the client's copy, and\ninventing it here would put an English word in a body every other string of which is\ntranslated."
          },
          "publicOnStatusPage": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "kind",
          "publicOnStatusPage",
          "createdAt"
        ]
      },
      "EventActor": {
        "type": "object",
        "description": "A label rather than a user object.\n\nThe client renders one line of text, and the address is what identifies a colleague in a\nteam where two people share a first name.",
        "properties": {
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "label"
        ]
      },
      "ListIncidentsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Incident"
            }
          }
        }
      },
      "GetIncidentResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/IncidentDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "IncidentTimeline": {
        "type": "object",
        "description": "An incident's timeline, with enough of the incident itself that a client rendering the page\ndoes not have to fetch it twice.",
        "properties": {
          "incidentId": {
            "type": "string"
          },
          "monitor": {
            "$ref": "#/components/schemas/EventMonitor"
          },
          "status": {
            "type": "string"
          },
          "startedAt": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IncidentEvent"
            }
          }
        },
        "required": [
          "incidentId",
          "monitor",
          "status",
          "startedAt"
        ]
      },
      "EventMonitor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ListIncidentEventsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/IncidentTimeline"
          }
        },
        "required": [
          "data"
        ]
      },
      "MaintenanceWindow": {
        "type": "object",
        "description": "A scheduled maintenance window.\n\nRead under `incident:read`, not a configuration permission: reading a window is reading the\nincident story, so a responder can see why they were not paged.",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          },
          "monitorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which monitors the window covers. Empty means the whole workspace."
          },
          "startsAt": {
            "type": "string"
          },
          "endsAt": {
            "type": "string"
          },
          "recurrenceRule": {
            "type": "string",
            "description": "An RFC 5545 rule for a repeating window, or null for a one-off."
          },
          "timezone": {
            "type": "string",
            "description": "The zone the rule is evaluated in. A weekly window at 02:00 has to stay at 02:00 across a\ndaylight-saving change, which a UTC offset cannot express."
          },
          "keepChecking": {
            "type": "boolean",
            "description": "Whether checks keep running during the window. They usually should: the point is to stop\npaging somebody, not to stop measuring."
          },
          "showOnStatusPage": {
            "type": "boolean"
          },
          "notifySubscribers": {
            "type": "boolean"
          },
          "cancelledAt": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "nextStartsAt": {
            "type": "string",
            "description": "The next occurrence, expanded from `recurrence_rule` — the one a reader should be shown.\n\n`starts_at` and `ends_at` above are the row's own pair, which for a repeating window is its\n**first** occurrence and nothing else. A client that printed those told somebody the work\nhappened in June while the window is in fact holding alerts back tonight, which is what both\nnative apps did before these two fields existed.\n\nThe occurrence in progress when there is one, because \"next\" to somebody reading during the\nwork is the work they can see. Both are **absent when there is no next occurrence**: a series\nthat has ended, or a rule outside the subset the server expands. Absent is not \"the same as\nthe stored pair\" — falling back to it is the bug — so a client says there is nothing due\nrather than guessing a date."
          },
          "nextEndsAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "title",
          "workspaceId",
          "startsAt",
          "endsAt",
          "timezone",
          "keepChecking",
          "showOnStatusPage",
          "notifySubscribers",
          "createdAt"
        ]
      },
      "ListMaintenanceResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MaintenanceWindow"
            }
          }
        }
      },
      "IndexResponse": {
        "type": "object",
        "description": "`GET /api/v1` — what this key is and where to go next.",
        "properties": {
          "version": {
            "type": "string"
          },
          "organization": {
            "$ref": "#/components/schemas/IndexOrganization"
          },
          "scope": {
            "type": "string",
            "description": "What this key can see, as a JSON object: `{\"workspaces\": \"all\" | <count>, \"role\": \"...\"}`.\n\nTold plainly because a key confined to one workspace that silently returns fewer monitors\nthan the dashboard is a support ticket; saying so here turns it into a fact the caller can\ncheck.\n\nIt is a string of JSON rather than a message for one unhappy reason: `workspaces` is either\nthe word `all` or a count, and a field that is sometimes a string and sometimes a number has\nno protobuf type. It is published and clients read it, so it is carried as it is rather than\nquietly changed — see `apps/api/PORTING.md`."
          },
          "documentation": {
            "type": "string",
            "description": "Where the prose lives. A caller who has hit an unexpected 403, or is wondering why `config`\nis absent, needs the reasons, and those cannot fit in this payload."
          },
          "endpoints": {
            "type": "string",
            "description": "Name to path, and path to the verbs it answers, both as JSON objects.\n\nNot protobuf maps: a map is unordered, and this is a reference somebody reads top to bottom\nin the order the endpoints were written down. A `HashMap` would reshuffle it on every\nrequest and a `BTreeMap` would alphabetise it, putting `account` above `monitors`."
          },
          "methods": {
            "type": "string"
          },
          "mcp": {
            "type": "string",
            "description": "The Model Context Protocol endpoint, for an agent holding this key.\n\nAdvertised here because an agent that already has a key and reads this index is the one\ncaller who can act on it without a person reading a page first — which is the whole\ndifficulty with a feature an agent is meant to find.\n\nRead-only: its tools are this API's `GET` operations and nothing behind it builds another\nmethod. The key still decides what those reads return."
          }
        },
        "required": [
          "version",
          "organization",
          "scope",
          "documentation",
          "endpoints",
          "methods",
          "mcp"
        ]
      },
      "IndexOrganization": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "plan"
        ]
      },
      "Me": {
        "type": "object",
        "description": "`GET /api/v1/me` — who is asking, and everything a client needs to render a shell.\n\nDeliberately one response rather than several: it is fetched on every cold start, and a phone\non a bad connection pays for round trips, not for bytes.",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "organization": {
            "$ref": "#/components/schemas/MeOrganization"
          },
          "organizations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Membership"
            },
            "description": "The organisation switcher, and it is **empty for a key**. A key is issued against one\nmembership and confined to it, so listing the others would advertise organisations that\ncredential cannot reach."
          },
          "membership": {
            "$ref": "#/components/schemas/MeMembership"
          },
          "entitlements": {
            "type": "string",
            "description": "The plan's limits and features with the organisation's add-on packs already folded in, so a\npack bought five minutes ago is reflected without shipping a new build.\n\nA JSON object rather than a message, for two reasons that both matter. The limits table is\n**generated** from `packages/core/src/plans.ts`, so a hand-written message here would be a\nsecond copy of a generated file, drifting the first time a limit is added — and in it `null`\nmeans *unlimited*, which no protobuf scalar can carry and which a client reading a missing\nfield as zero would turn into an account allowed nothing."
          },
          "usage": {
            "type": "string",
            "description": "Cached counts behind the usage bars, refreshed hourly. **Empty for an API key**, exactly as\nthe TypeScript leaves it: the counts exist for the dashboard's bars, and a key that fetched\nthem would be a per-request join for a value nothing on that path reads."
          },
          "billing": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MeBilling"
              }
            ],
            "description": "Where the money comes from, or null on the free plan. A client cannot work this out for\nitself and each answer is a different screen."
          }
        },
        "required": [
          "user",
          "organization",
          "membership",
          "entitlements",
          "usage"
        ]
      },
      "User": {
        "type": "object",
        "description": "Who to print when this actor appears in a timeline, an audit row or here.\n\nFor a key the name is the *key's* name, not \"API key\", and the email stays empty. A key is not\na person and must not be mistaken for one — but \"Closed by .\" is what an empty name produced\nin an incident timeline, which is worse than useless in a review.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "image": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "email"
        ]
      },
      "MeOrganization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "plan"
        ]
      },
      "Membership": {
        "type": "object",
        "description": "One row of the organisation switcher.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "plan",
          "role"
        ]
      },
      "MeMembership": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string"
          },
          "workspaceIds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StringList"
              }
            ],
            "description": "Null means the whole organisation; a list is a confined member — an agency's client user —\nwho must never be shown a workspace picker offering more than this."
          }
        },
        "required": [
          "role"
        ]
      },
      "MeBilling": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "The column, not a guess from which id happens to be populated. The app decides whether to\noffer a purchase on this value, so getting it wrong sells a second subscription to somebody\nalready paying."
          },
          "renewsAt": {
            "type": "string"
          },
          "inGracePeriod": {
            "type": "boolean",
            "description": "The card is being retried. Access continues, and the copy has to say why — silently losing\nthe plan at the end of a grace period is the version that generates a ticket."
          }
        },
        "required": [
          "source",
          "inGracePeriod"
        ]
      },
      "GetMeResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Me"
          }
        },
        "required": [
          "data"
        ]
      },
      "OrganizationChoice": {
        "type": "object",
        "description": "An organisation the signed-in person belongs to, and whether it is the one this session\ncurrently resolves to.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "description": "The role held there, which is not the role held here."
          },
          "active": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "plan",
          "role",
          "active"
        ]
      },
      "ListOrganizationsResponse": {
        "type": "object",
        "description": "`GET /api/v1/organizations`",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrganizationChoice"
            }
          }
        }
      },
      "ActiveOrganizationResponse": {
        "type": "object",
        "description": "`PUT /api/v1/organizations/active` — the membership now in use.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Membership"
          }
        },
        "required": [
          "data"
        ]
      },
      "Monitor": {
        "type": "object",
        "description": "One monitor, as every read of one returns it.\n\n## Why `status` is a string and not an enum\n\nA protobuf enum is a closed set compiled into the client, and a value it has never heard of\narrives as `UNKNOWN` — which for a monitoring product means a customer's dashboard showing a\nstate it cannot name, silently, on the day a state is added. The string is what the JSON has\nalways carried, and a client that does not recognise one can at least print it.\n\nThe same reasoning covers `kind`: thirty today, and adding a thirty-first is a migration\nplus a checker, not a client release.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "One of the thirty monitor kinds: `http`, `ping`, `tcp`, `dns`, `ssl_cert`, `postgres`,\n`docker`, and so on."
          },
          "status": {
            "type": "string",
            "description": "`up`, `degraded`, `down`, `maintenance`, `paused`, or `pending`.\n\nA monitor a human paused reads `paused` whatever its last check said, and one with no\nstate row yet reads `pending` rather than `up` — claiming something is up before checking\nit is the one lie a monitoring product must never tell."
          },
          "statusSince": {
            "type": "string",
            "description": "When it entered that state. RFC 3339, milliseconds, `Z` — the same string\n`Date.prototype.toISOString()` prints, because a client parsing both stacks during the\ncutover must not see two formats."
          },
          "enabled": {
            "type": "boolean"
          },
          "intervalSeconds": {
            "type": "integer",
            "format": "int32"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "lastCheckedAt": {
            "type": "string"
          },
          "lastResponseTimeMs": {
            "type": "integer",
            "format": "int32"
          },
          "lastMessage": {
            "type": "string",
            "description": "The one substantive line of the last failure, already in the organisation's language."
          },
          "uptime24h": {
            "type": "number",
            "format": "float",
            "description": "Percentages, `null` where nothing was measured in the window.\n\n**Absent is not zero.** A monitor with no checks in the last 24 hours has *unknown*\nuptime, and a client that rendered a missing value as 0% would put a brand-new monitor on\na dashboard at nought per cent."
          },
          "uptime30d": {
            "type": "number",
            "format": "float"
          },
          "openIncidentId": {
            "type": "string",
            "description": "Set while an incident is open against this monitor."
          },
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostLink"
            },
            "description": "The machines this monitor runs on, if any have been recorded.\n\n**Empty is the ordinary case and says nothing is wrong.** Hosts are an optional grouping:\na monitor that names none is checked, alerted on and reported exactly as one that names\nthree. One host means a failure here is evidence about that machine; more than one means\nthe check answers from several, and it colours none of them."
          },
          "anomaly": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MonitorAnomaly"
              }
            ],
            "description": "Set while this monitor is answering far slower than its own recent history; `null`\notherwise, which is the ordinary state.\n\nNot an incident and not a status: the monitor is still up. It names no region and never\nwill — how many vantage points agreed is ours to know, not a claim to publish."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "status",
          "enabled",
          "intervalSeconds",
          "workspace"
        ]
      },
      "MonitorAnomaly": {
        "type": "object",
        "description": "A response-time anomaly that is still open.",
        "properties": {
          "since": {
            "type": "string",
            "description": "When it opened. RFC 3339, milliseconds, `Z`."
          },
          "baselineMs": {
            "type": "integer",
            "format": "int32",
            "description": "What this monitor usually answers in, in milliseconds."
          },
          "observedMs": {
            "type": "integer",
            "format": "int32",
            "description": "What it answered in when the anomaly opened, in milliseconds."
          }
        },
        "required": [
          "since",
          "baselineMs",
          "observedMs"
        ]
      },
      "ListMonitorsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Monitor"
            }
          }
        }
      },
      "MonitorDetail": {
        "type": "object",
        "description": "One monitor, with the two fields the collection endpoint withholds from everybody.\n\nThe field order repeats `Monitor` rather than embedding it: `paused` sits between `enabled`\nand `intervalSeconds` in the body this replaces, and a nested message would move it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "statusSince": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "paused": {
            "type": "boolean",
            "description": "Paused by a person, as opposed to disabled. A paused monitor reads `paused` whatever its\nlast check said."
          },
          "intervalSeconds": {
            "type": "integer",
            "format": "int32"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "lastCheckedAt": {
            "type": "string"
          },
          "lastResponseTimeMs": {
            "type": "integer",
            "format": "int32"
          },
          "lastMessage": {
            "type": "string"
          },
          "uptime24h": {
            "type": "number",
            "format": "float"
          },
          "uptime30d": {
            "type": "number",
            "format": "float"
          },
          "openIncidentId": {
            "type": "string"
          },
          "config": {
            "type": "string",
            "description": "The monitor's own configuration, as a JSON object — **absent unless the caller could\nrewrite it anyway**.\n\nIt holds request headers and basic-auth credentials for some kinds, so a read-scoped key\nmust not be a way to read back what somebody typed into the monitor form. But withholding it\nfrom an editor makes editing impossible rather than safe: a client with no way to read the\ncurrent configuration can only rebuild it from defaults, which silently drops `authRef` and\nevery header the monitor depends on — a check that still runs and quietly stops\nauthenticating."
          },
          "lastCheckDetail": {
            "type": "string",
            "description": "The last check's own findings, for the kinds that produce any — SPF and DMARC for a mail\nposture monitor, the protocol and cipher probes for a TLS audit.\n\nThree states, and all three are meaningful. **Absent** means this kind never reports\nfindings. The literal JSON `null` means it does and has not run yet. Anything else is the\ndocument. A client can therefore tell \"nothing found yet\" from \"this kind does not report\nfindings\" without keeping its own copy of the list."
          },
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostLink"
            },
            "description": "The machines this monitor runs on, if any. Empty is the ordinary case — see `Monitor`."
          },
          "anomaly": {
            "allOf": [
              {
                "$ref": "#/components/schemas/MonitorAnomaly"
              }
            ],
            "description": "Set while a response-time anomaly is open, `null` otherwise — see `Monitor`."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "status",
          "enabled",
          "paused",
          "intervalSeconds",
          "workspace"
        ]
      },
      "GetMonitorResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MonitorDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "HistoryDay": {
        "type": "object",
        "description": "One day of the ninety-day bar.",
        "properties": {
          "date": {
            "type": "string"
          },
          "uptime": {
            "type": "number",
            "format": "double",
            "description": "Null for a day with no rollup. A client given only the days that have data would draw a bar\nthat quietly gets shorter during an outage — the gap is the interesting part."
          }
        },
        "required": [
          "date"
        ]
      },
      "MonitorHistory": {
        "type": "object",
        "description": "Daily uptime over a window.\n\n**Daily rollups only, never raw check results** — retention deletes those out from under\nanything that computes from them, so a figure derived from raw rows silently changes meaning\nat the retention boundary.",
        "properties": {
          "monitorId": {
            "type": "string"
          },
          "days": {
            "type": "integer",
            "format": "int32"
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryDay"
            }
          }
        },
        "required": [
          "monitorId",
          "days"
        ]
      },
      "GetMonitorHistoryResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MonitorHistory"
          }
        },
        "required": [
          "data"
        ]
      },
      "GraphBucket": {
        "type": "object",
        "description": "One hour of a monitor's short graph.",
        "properties": {
          "bucket": {
            "type": "string",
            "description": "The start of the hour. RFC 3339, milliseconds, `Z`."
          },
          "uptime": {
            "type": "number",
            "format": "double",
            "description": "The share of that hour's checks that passed, with degraded counted as passing — the same rule\nas the ninety-day bar, so the two charts cannot disagree about one hour.\n\nSet on an `uptime` graph only, and **null for an hour with no checks**, which is not zero: a\ngap is an absence of knowledge and a zero is an outage."
          }
        },
        "required": [
          "bucket"
        ]
      },
      "GraphReading": {
        "type": "object",
        "description": "One hour of one measured series.",
        "properties": {
          "bucket": {
            "type": "string",
            "description": "The start of the hour. Every series on a graph carries the same hours as the graph itself."
          },
          "value": {
            "type": "number",
            "format": "double",
            "description": "The hour's average, and null for an hour with no reading. Null is not zero."
          },
          "min": {
            "type": "number",
            "format": "double",
            "description": "The extremes inside the hour. The maximum is what a client scales a `raw` series against,\nbecause a four-minute spike is what somebody opened the chart for and an average hides it."
          },
          "max": {
            "type": "number",
            "format": "double"
          }
        },
        "required": [
          "bucket"
        ]
      },
      "GraphSeries": {
        "type": "object",
        "description": "One named number a quantitative monitor measures.",
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable within the monitor: an OID for SNMP, `cpu`/`memory`/`disk`/`swap` for an agent,\n`bitsPerSecond`/`packetsPerSecond`/`flowsPerSecond` for a flow monitor."
          },
          "label": {
            "type": "string",
            "description": "What the customer named it. Null where they named nothing and the key has to do."
          },
          "unit": {
            "type": "string",
            "description": "`percent` when the number already is one, `raw` when nothing knows what it means. A client\ndraws a `raw` series against its own highest reading in the window and never prints that\nshare as though it were the reading."
          },
          "buckets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GraphReading"
            }
          }
        },
        "required": [
          "key",
          "unit"
        ]
      },
      "MonitorGraph": {
        "type": "object",
        "description": "A monitor's last hours, hour by hour — what the dashboard draws on a monitor row and page.\n\nTwo charts wearing one shape, because a monitor is one of two things. `uptime` graphs carry the\nshare of checks that passed in `buckets`; `value` graphs, for the quantitative kinds, carry\ntheir readings in `series`, and `buckets` then holds only the hours. **Hourly rollups only**,\nnever raw check results, for the retention reason `MonitorHistory` gives.",
        "properties": {
          "monitorId": {
            "type": "string"
          },
          "measure": {
            "type": "string",
            "description": "`uptime` or `value`."
          },
          "hours": {
            "type": "integer",
            "format": "int32",
            "description": "How many hours the graph covers."
          },
          "buckets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GraphBucket"
            },
            "description": "Every hour in the window, oldest first, whether or not anything was recorded in it — a\nfixed width, so a gap is visibly a gap."
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GraphSeries"
            },
            "description": "Empty for an `uptime` graph. Each series is padded onto the same hours as `buckets`."
          }
        },
        "required": [
          "monitorId",
          "measure",
          "hours"
        ]
      },
      "GetMonitorGraphResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MonitorGraph"
          }
        },
        "required": [
          "data"
        ]
      },
      "ListMonitorGraphsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MonitorGraph"
            }
          }
        }
      },
      "MonitorChannels": {
        "type": "object",
        "description": "Which channels a monitor alerts through.",
        "properties": {
          "monitorId": {
            "type": "string"
          },
          "channelIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "usingWorkspaceDefaults": {
            "type": "boolean",
            "description": "Said explicitly rather than left to be inferred from an empty array.\n\nAn empty list does not mean nobody is alerted: it means the monitor falls back to the\nworkspace's default set. A client that showed \"no channels\" for that would be telling\nsomebody their monitor is silent when it is not."
          }
        },
        "required": [
          "monitorId",
          "usingWorkspaceDefaults"
        ]
      },
      "GetMonitorChannelsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MonitorChannels"
          }
        },
        "required": [
          "data"
        ]
      },
      "PushUrl": {
        "type": "object",
        "description": "A heartbeat monitor's ping URL.",
        "properties": {
          "monitorId": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Null for a kind that is not pinged, and for a token no key in the ring can open any more.\nThe monitor keeps working in the second case; the URL simply cannot be shown, and rotating\nit issues a new one."
          }
        },
        "required": [
          "monitorId"
        ]
      },
      "GetPushUrlResponse": {
        "type": "object",
        "description": "`GET /api/v1/monitors/{id}/push-url`",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PushUrl"
          }
        },
        "required": [
          "data"
        ]
      },
      "OnCallView": {
        "type": "object",
        "description": "Who is on call, the rotations behind that, and the escalation policies.\n\nThe rotation is **computed from the clock**, never stored. There is no nightly job to advance\nit, so there is no nightly job that can fail and leave nobody on call: the position falls out\nof how much time has passed since `rotation_starts_at`. An override wins over the computed\nanswer for its window, which is the only piece of state involved.\n\nComputed by the same function the notifier pages through. An API that named a different person\nfrom the one the pager actually reaches would be worse than no API — it is the screen somebody\nchecks before going to bed.",
        "properties": {
          "onCallNow": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnCallEntry"
            },
            "description": "The headline, first and separately: who is on call at this instant, across every schedule. A\nclient rendering one line at the top of a screen should not have to walk the schedules to\nassemble it."
          },
          "schedules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnCallSchedule"
            }
          },
          "policies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EscalationPolicy"
            }
          }
        }
      },
      "OnCallEntry": {
        "type": "object",
        "description": "One person, at this instant.\n\nBoth fields are nullable because a schedule can resolve to nobody: an empty participant list,\nor a user id that no longer has a membership. That is a state worth showing rather than\nhiding — a rotation with nobody on it pages nobody.",
        "properties": {
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "The address, or the name where there is no address. What identifies a colleague in a team\nwhere two people share a first name."
          },
          "viaOverride": {
            "type": "boolean",
            "description": "True when a cover override is what put them there, rather than the rotation."
          },
          "scheduleId": {
            "type": "string",
            "description": "Set only in the top-level list, where one entry has to say which schedule it came from. The\ncopy nested inside a schedule already knows."
          }
        },
        "required": [
          "viaOverride"
        ]
      },
      "OnCallSchedule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "rotationHours": {
            "type": "integer",
            "format": "int32",
            "description": "24 daily, 168 weekly, 336 fortnightly. Every handover is computed from the start."
          },
          "rotationStartsAt": {
            "type": "string"
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnCallParticipant"
            }
          },
          "current": {
            "allOf": [
              {
                "$ref": "#/components/schemas/OnCallEntry"
              }
            ],
            "description": "Null where the rotation resolves to nobody."
          },
          "overrides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnCallOverride"
            }
          },
          "unreachable": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UnreachableParticipant"
            },
            "description": "People on this rotation an escalation currently cannot reach. Usually empty.\n\nIt is the quietest way this feature fails: somebody is on the rotation, the ladder reaches\nthem, and there is no enabled channel to reach them through. It costs nothing to say so now\nand everything to find out during an outage."
          }
        },
        "required": [
          "id",
          "name",
          "timezone",
          "rotationHours",
          "rotationStartsAt"
        ]
      },
      "OnCallParticipant": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "label"
        ]
      },
      "OnCallOverride": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "startsAt": {
            "type": "string"
          },
          "endsAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "userId",
          "label",
          "startsAt",
          "endsAt"
        ]
      },
      "UnreachableParticipant": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "email",
          "name"
        ]
      },
      "EscalationPolicy": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "repeatAfterSteps": {
            "type": "boolean",
            "description": "Loop back to the first step until somebody acknowledges, up to `max_repeats`."
          },
          "maxRepeats": {
            "type": "integer",
            "format": "int32"
          },
          "monitorCount": {
            "type": "integer",
            "format": "int32",
            "description": "How many monitors currently escalate through this policy. Zero means unused."
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EscalationStep"
            }
          }
        },
        "required": [
          "id",
          "name",
          "repeatAfterSteps",
          "maxRepeats",
          "monitorCount"
        ]
      },
      "EscalationStep": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "format": "int32"
          },
          "delaySeconds": {
            "type": "integer",
            "format": "int32",
            "description": "How long after the incident *opened* this step fires — an absolute offset, not a gap from\nthe previous step. Offsets are what people mean by \"nobody has picked it up after five\nminutes\"; gaps would make step three's real firing time depend on edits to steps one and\ntwo."
          },
          "channelIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scheduleIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "position",
          "delaySeconds"
        ]
      },
      "GetOnCallResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/OnCallView"
          }
        },
        "required": [
          "data"
        ]
      },
      "requests.NullableString": {
        "type": "object",
        "description": "A string a caller may also send as `null`.\n\nThree states, and all three are used: **absent** leaves the field alone, **null** clears it,\nand a value sets it. Without the middle one a status page headline is something that can be\nset and never taken away, and the bug reads as the API ignoring the request rather than\nfailing.",
        "properties": {
          "value": {
            "type": "string"
          }
        }
      },
      "requests.NullableInt64": {
        "type": "object",
        "description": "The same, for a number.",
        "properties": {
          "value": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "requests.CreateMonitorRequest": {
        "type": "object",
        "description": "`POST /api/v1/monitors`\n\n`workspace_id` is required rather than defaulted. Putting a monitor somewhere the caller did\nnot name means, on an agency account, a client seeing another client's monitor on their status\npage.",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          },
          "config": {
            "type": "string",
            "description": "The per-kind configuration, as a JSON object. Its schema is generated from the same zod\ndefinition both stacks validate against — thirty kinds and about 260 fields — so it is\ncarried whole rather than described twice."
          },
          "intervalSeconds": {
            "type": "integer",
            "format": "int32",
            "description": "Absent takes the default rather than the fastest thing the caller could have asked for. The\nplan floor is applied on the way in, so an interval below it is clamped and the response\nsays what was actually stored."
          },
          "confirmations": {
            "type": "integer",
            "format": "int32"
          },
          "dependsOn": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Monitors whose failure explains this one's. An empty list is the normal case."
          },
          "probeId": {
            "type": "string",
            "description": "One of the organisation's own private probes, which then checks this monitor alone.\nOptional for every kind but the four flow kinds — `netflow`, `sflow`, `jflow` and `ipfix` —\nwhich are refused without one (`private_probe_required`): the router exports to a collector\non the customer's network, so no other probe could ever receive the traffic."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Labels this monitor is grouped and filtered by, and on every plan — a tag is how somebody\nwith two hundred checks finds the nine that are one customer's.\n\nA **bare** repeated field, unlike the patch's. On a create there is nothing to leave alone,\nso \"absent\" and \"empty\" are the same request and the distinction protobuf cannot express is\none nobody needs here.\n\nFolded to lower case, trimmed, inner whitespace collapsed; blanks and duplicates dropped.\nRefused rather than trimmed where dropping would lose the caller's meaning: a tag over 32\ncharacters (`tag_too_long`), more than ten of them (`too_many_tags`), and a **comma**\n(`tag_cannot_contain_comma`), because `?tag=a,b` is this API's own separator and a tag\nholding one could never be filtered for."
          }
        }
      },
      "requests.UpdateMonitorRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/monitors/{id}`\n\nPausing is applied first and separately: it is its own permission, and a key allowed to pause\nbut not to update should still be able to do the thing it is allowed to do.",
        "properties": {
          "name": {
            "type": "string"
          },
          "intervalSeconds": {
            "type": "integer",
            "format": "int32"
          },
          "confirmations": {
            "type": "integer",
            "format": "int32"
          },
          "enabled": {
            "type": "boolean"
          },
          "paused": {
            "type": "boolean"
          },
          "config": {
            "type": "string"
          },
          "tags": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StringList"
              }
            ],
            "description": "The monitor's whole tag list, replaced — absent leaves it alone.\n\n**Wrapped, and that is the point.** Proto3 cannot tell an unset `repeated` field from an\nempty one, so a bare `repeated string` here would make \"take every tag off this monitor\"\ninexpressible: the empty list would arrive as the absent one and the write would leave the\ntags where they were, reporting success. The wrapper carries both, exactly as\n`SetMonitorChannelsRequest.channel_ids` does and for the same reason, and its JSON is still\nthe bare array rather than `{\"values\": [...]}`.\n\nReplaced whole rather than added to or removed from, because a tag has no identity apart\nfrom its own text — the string *is* the tag. Same validation as the create's."
          }
        }
      },
      "requests.UpdateIncidentRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/incidents/{id}` — acknowledge, resolve, or comment.\n\nA status word rather than two endpoints. There is no way back to `open`: un-acknowledging\nwould mean restarting an escalation ladder from a step nobody can reconstruct, and reopening a\nresolved incident would hide a second outage inside the first one's duration.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`acknowledged` or `resolved`."
          },
          "comment": {
            "type": "string",
            "description": "Written **first**, before any status change. If both are sent and the status change fails,\nthe note explaining why somebody was closing the incident is still on the timeline."
          },
          "publish": {
            "type": "boolean",
            "description": "Whether the comment also goes on the status page. A second permission, because a comment is\nan internal note and a status-page update is a statement to the public."
          }
        }
      },
      "requests.SavePostmortemRequest": {
        "type": "object",
        "description": "`PUT /api/v1/incidents/{id}/postmortem` — write or replace an incident's postmortem.\n\nA whole document rather than a patch: there is one per incident and the editor holds all of\nit, so the last save is the postmortem.",
        "properties": {
          "body": {
            "type": "string",
            "description": "Markdown, trimmed, 1 to 50,000 characters."
          }
        }
      },
      "requests.CreateWorkspaceRequest": {
        "type": "object",
        "description": "`POST /api/v1/workspaces`",
        "properties": {
          "name": {
            "type": "string"
          },
          "clientReference": {
            "type": "string",
            "description": "The agency's own label for the client — an account number, a contract reference. Never\nrendered on anything a client sees."
          }
        }
      },
      "requests.CreateApiKeyRequest": {
        "type": "object",
        "description": "`POST /api/v1/api-keys`",
        "properties": {
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Empty means everything the owner's role allows, which is the common case."
          },
          "workspaceIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expiresInDays": {
            "allOf": [
              {
                "$ref": "#/components/schemas/requests.NullableInt64"
              }
            ],
            "description": "**Absent means ninety days, not \"never\".** A key with no expiry is a key nobody ever\nrevokes. Sending an explicit null is how a caller asks for one, so the choice is theirs and\ndeliberate rather than a default they never saw."
          }
        }
      },
      "requests.RevokeApiKeyRequest": {
        "type": "object",
        "description": "`DELETE /api/v1/api-keys` — the body form. `?id=` is the documented one; a body is accepted\nbecause several HTTP clients make a DELETE with a query string awkward and one with a body\neasy.",
        "properties": {
          "id": {
            "type": "string"
          }
        }
      },
      "requests.CreateChannelRequest": {
        "type": "object",
        "description": "`POST /api/v1/channels`",
        "properties": {
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "target": {
            "type": "string",
            "description": "The address, webhook URL, chat id or number — whatever this kind is aimed at."
          },
          "workspaceId": {
            "allOf": [
              {
                "$ref": "#/components/schemas/requests.NullableString"
              }
            ],
            "description": "Null is \"the whole organisation\", which differs from absent only in that the caller said so\ndeliberately."
          }
        }
      },
      "requests.UpdateChannelRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/channels/{id}` — enable or disable one.",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "requests.InviteMemberRequest": {
        "type": "object",
        "description": "`POST /api/v1/members`\n\nThe invitation link is not returned: it goes in the email and nowhere else, so a key that can\ninvite is not thereby a key that can accept.",
        "properties": {
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "workspaceIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Empty is the whole organisation; a list confines them, which is what an agency's client user\nis."
          }
        }
      },
      "requests.UpdateMemberRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/members/{id}` — change somebody's role.",
        "properties": {
          "role": {
            "type": "string"
          }
        }
      },
      "requests.CreateStatusPageRequest": {
        "type": "object",
        "description": "`POST /api/v1/status-pages`",
        "properties": {
          "name": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          }
        }
      },
      "requests.UpdateStatusPageRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/status-pages/{id}`\n\nFive fields are nullable because clearing them is a thing somebody does: a headline written\nfor one incident, a logo from a rebrand, a password that should no longer be asked for.",
        "properties": {
          "name": {
            "type": "string"
          },
          "headline": {
            "$ref": "#/components/schemas/requests.NullableString"
          },
          "description": {
            "$ref": "#/components/schemas/requests.NullableString"
          },
          "logoUrl": {
            "$ref": "#/components/schemas/requests.NullableString"
          },
          "themeAccent": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "historyDays": {
            "type": "integer",
            "format": "int64"
          },
          "showResponseTimes": {
            "type": "boolean"
          },
          "showIncidentHistory": {
            "type": "boolean"
          },
          "subscribersEnabled": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string",
            "description": "`public`, `password` or `private`."
          },
          "password": {
            "allOf": [
              {
                "$ref": "#/components/schemas/requests.NullableString"
              }
            ],
            "description": "Sending null removes the password, which is how a page goes from `password` back to open."
          },
          "customCss": {
            "$ref": "#/components/schemas/requests.NullableString"
          },
          "hideVitrinaBranding": {
            "type": "boolean"
          },
          "groupByHost": {
            "type": "boolean",
            "description": "Group the public page by the machine each component runs on. Publishes host names, so it\nis never turned on for a caller who did not ask."
          }
        }
      },
      "requests.AddComponentRequest": {
        "type": "object",
        "description": "`POST /api/v1/status-pages/{id}/components` — publish a monitor on a page.\n\nThe check behind this is that the monitor is in **the page's** workspace, not merely in the\norganisation: otherwise an agency can publish one client's monitor on another client's page.",
        "properties": {
          "monitorId": {
            "type": "string"
          },
          "displayName": {
            "type": "string",
            "description": "The public name. Absent takes the monitor's own."
          }
        }
      },
      "requests.RemoveComponentRequest": {
        "type": "object",
        "description": "`DELETE /api/v1/status-pages/{id}/components` — the body form of `?component=`.",
        "properties": {
          "componentId": {
            "type": "string"
          }
        }
      },
      "requests.EnrolAgentRequest": {
        "type": "object",
        "description": "`POST /api/v1/agents`",
        "properties": {
          "name": {
            "type": "string"
          },
          "workspaceId": {
            "allOf": [
              {
                "$ref": "#/components/schemas/requests.NullableString"
              }
            ],
            "description": "A named workspace is checked against the caller before the write; an absent one falls back\nto the default. A confined member naming somebody else's workspace is refused rather than\nquietly getting the default."
          }
        }
      },
      "requests.RetireAgentRequest": {
        "type": "object",
        "description": "`DELETE /api/v1/agents` — the body form of `?id=`.",
        "properties": {
          "id": {
            "type": "string"
          }
        }
      },
      "requests.CreateMaintenanceRequest": {
        "type": "object",
        "description": "`POST /api/v1/maintenance`",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          },
          "monitorIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which monitors the window covers. Empty means the whole workspace.\n\nAn id that is not a uuid is **refused** rather than dropped: a typo silently narrowing the\nwindow means the alert it was created to suppress fires anyway."
          },
          "startsAt": {
            "type": "string",
            "description": "ISO-8601. A date alone is accepted and read as midnight UTC, which is what\n`new Date(\"2026-01-01\")` does."
          },
          "endsAt": {
            "type": "string"
          },
          "recurrenceRule": {
            "type": "string",
            "description": "An RFC 5545 rule for a repeating window."
          },
          "timezone": {
            "type": "string",
            "description": "The zone the rule is evaluated in. A weekly window at 02:00 has to stay at 02:00 across a\ndaylight-saving change, which a UTC offset cannot express."
          },
          "keepChecking": {
            "type": "boolean",
            "description": "Whether checks keep running. They usually should: the point is to stop paging somebody, not\nto stop measuring."
          },
          "showOnStatusPage": {
            "type": "boolean"
          },
          "notifySubscribers": {
            "type": "boolean"
          }
        }
      },
      "requests.SetMonitorChannelsRequest": {
        "type": "object",
        "description": "`PUT /api/v1/monitors/{id}/channels`\n\nThe whole list, never an add-one or a remove-one: a client that reads the list, edits it and\nwrites it back cannot race itself into a half-applied routing change, which for this setting\nmeans an alert going nowhere.\n\n**Empty and absent are different.** Empty means \"fall back to the workspace defaults\" and is\naccepted; a body that names no list at all is refused, because a caller who sent nothing did\nnot ask for that. Protobuf cannot tell an unset `repeated` field from an empty one, so the\nlist is wrapped.",
        "properties": {
          "channelIds": {
            "$ref": "#/components/schemas/StringList"
          }
        }
      },
      "requests.VerifyAppleRequest": {
        "type": "object",
        "description": "`POST /api/v1/billing/apple/verify`\n\n**An id and nothing else that is believed.** The product, the plan, the expiry, the\nenvironment and the bundle are read back from the App Store Server API over a signed channel.\nA client that could name its own plan here would be a client that grants itself Business for\nfree.",
        "properties": {
          "transactionId": {
            "type": "string",
            "description": "The StoreKit transaction id: a decimal string of at most 32 digits."
          }
        }
      },
      "requests.UploadSourceMapRequest": {
        "type": "object",
        "description": "`POST /api/v1/sourcemaps`\n\nOne request with a raw body rather than Sentry's chunked artifact-bundle protocol. That\nprotocol exists to make `sentry-cli` fast against very large uploads and it is a lot of\nsurface; this is a `curl` in a build script, which is what somebody actually needs at the\npoint they discover their stack traces are unreadable.",
        "properties": {
          "projectRef": {
            "type": "integer",
            "format": "int64",
            "description": "The number in the DSN, which is what a build script has to hand. The project is resolved\ninside the caller's own organisation: a source map is the customer's original source, which\nis exactly what makes uploading into somebody else's project worth being careful about."
          },
          "filename": {
            "type": "string"
          },
          "debugId": {
            "type": "string",
            "description": "From the build. Lowercased on the way in, because the toolchains disagree about case and a\nlookup that should be exact would quietly miss."
          },
          "release": {
            "type": "string"
          },
          "map": {
            "type": "string",
            "description": "The map itself, as a JSON document. Compressed on the way in."
          },
          "mapGzipBase64": {
            "type": "string",
            "description": "Or the same thing already gzipped, base64-encoded — which is what a script uploading a\nfive-megabyte map should send."
          }
        }
      },
      "requests.UpdateIssueRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/errors/issues/{id}` — resolve, ignore, or reopen.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`unresolved`, `resolved` or `ignored`. `unresolved` is also how one is reopened."
          },
          "ignoreHours": {
            "type": "integer",
            "format": "int64",
            "description": "How long to ignore it for, capped at ninety days. Absent is \"until I say otherwise\"; a\nwindow is \"stop paging me about this until Monday\". Both are ignoring it, and the difference\nis whether it comes back on its own."
          }
        }
      },
      "requests.CreateHostRequest": {
        "type": "object",
        "description": "`POST /api/v1/hosts`\n\nOnly `name` is required, and it is the only field the product reads. Everything else is the\noperator's own note about the machine.",
        "properties": {
          "name": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "requests.UpdateHostRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/hosts/{id}`\n\nEvery field is sent whole rather than merged: a host is four strings a person typed, and a\nthree-state \"absent leaves it alone\" on each would be ceremony around a form with no\nexpensive fields in it. An omitted field clears.",
        "properties": {
          "name": {
            "type": "string"
          },
          "provider": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        }
      },
      "requests.SetHostMonitorRequest": {
        "type": "object",
        "description": "`POST /api/v1/hosts/{id}/monitors` and its `DELETE` body — one monitor on or off one host.\n\nPer-monitor rather than a whole set, deliberately, and the opposite of\n`PUT /api/v1/monitors/{id}/hosts`: a host on an agency account carries monitors from several\nworkspaces, so a caller replacing \"the list\" would be replacing rows it was never shown.",
        "properties": {
          "monitorId": {
            "type": "string"
          }
        }
      },
      "requests.SetMonitorHostsRequest": {
        "type": "object",
        "description": "`PUT /api/v1/monitors/{id}/hosts` — the whole list, and safe as a whole list because these\nlinks belong to one monitor.\n\nAn empty array is a legitimate request and means \"this monitor runs on no host in\nparticular\", which is where most monitors are and where they are free to stay.",
        "properties": {
          "hostIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "requests.UpdateWorkspaceRequest": {
        "type": "object",
        "description": "`PATCH /api/v1/workspaces/{id}`",
        "properties": {
          "name": {
            "type": "string"
          },
          "clientReference": {
            "allOf": [
              {
                "$ref": "#/components/schemas/requests.NullableString"
              }
            ],
            "description": "Absent leaves it alone, null clears it, a value sets it."
          }
        }
      },
      "requests.StartSmsEnrolmentRequest": {
        "type": "object",
        "description": "`POST /api/v1/sms/enrolment` — the caller's own number, never anybody else's.",
        "properties": {
          "phone": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "requests.ConfirmSmsEnrolmentRequest": {
        "type": "object",
        "description": "`POST /api/v1/sms/enrolment/confirm`",
        "properties": {
          "code": {
            "type": "string"
          }
        }
      },
      "requests.SetActiveOrganizationRequest": {
        "type": "object",
        "description": "`PUT /api/v1/organizations/active`",
        "properties": {
          "organizationId": {
            "type": "string"
          }
        }
      },
      "SearchHit": {
        "type": "object",
        "description": "One thing somebody was looking for.\n\nSearch spans the whole account rather than one resource, so a hit is deliberately shallow:\nenough to recognise the thing and navigate to it, and nothing more. A client that wants the\nrest fetches the resource's own endpoint by `id`, where that resource's own permission check\nand its own shape apply.\n\nNothing here is ever a secret. A monitor's `config` can hold an Authorization header, an API\nkey's hash is the credential itself, a channel's config is sealed, and a heartbeat token is\ndisplayable only on the monitor it belongs to — none of them are matched against and none of\nthem are returned. What is matched is what somebody would plausibly type: a name, a title, a\nhostname, a slug, an address, a tag, the displayable prefix of a key.",
        "properties": {
          "type": {
            "type": "string",
            "description": "Which kind of thing this is, as a stable snake_case identifier: `monitor`, `incident`,\n`postmortem`, `host`, `status_page`, `workspace`, `channel`, `maintenance`,\n`error_project`, `error_issue`, `analytics_site`, `member`, `invitation`, `api_key`,\n`agent` or `probe`.\n\nA string rather than an enum, for the reason every other open set in this API is one: a\nclient built against today's list must not fail to parse a kind added after it shipped. An\nunrecognised value is a hit a client may show generically or skip, not a decoding error."
          },
          "id": {
            "type": "string",
            "description": "The row's own id, in that resource's own namespace. `type` plus `id` is what a client turns\ninto a link — there is no URL here, because the path a hit opens at is the client's\ndecision and differs between the dashboard and the two handsets."
          },
          "title": {
            "type": "string",
            "description": "The line that names the thing: a monitor's name, an incident's title, a member's name, an\nissue's message."
          },
          "subtitle": {
            "type": "string",
            "description": "The most useful second line, when there is one — the workspace a monitor sits in, the\nmonitor an incident is about, the domain of an analytics site, the displayable prefix of an\nAPI key. Absent where the title says everything.\n\nNever a credential and never a sealed value; see the note on the message."
          },
          "status": {
            "type": "string",
            "description": "The thing's own status where it has one — a monitor's `up`/`down`/`degraded`, an incident's\n`open`/`resolved`, an issue's `unresolved`. Absent for everything that does not have one,\nwhich is most kinds.\n\nNull is not healthy. A client must not render a missing status as `up`, for the same reason\na host with nothing pinned to it reports null rather than green."
          },
          "workspace": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Workspace"
              }
            ],
            "description": "The workspace the hit belongs to, for the kinds that have one.\n\nAbsent for the organisation-wide kinds — a host, an API key, a member, a private probe —\nwhich belong to the account rather than to one workspace. On an agency account this is what\ntells two identically named monitors apart, which is the case the field exists for."
          }
        },
        "required": [
          "type",
          "id",
          "title"
        ]
      },
      "SearchResponse": {
        "type": "object",
        "description": "Everything matched, across every kind the caller is allowed to see.\n\nOne response rather than one request per kind: a search box issues this on a keystroke, and\nsixteen round trips per keystroke is a different product. The per-kind result count is capped\nserver-side, so this is bounded however common the term is — a search is a way to find\nsomething, not a way to page through an account.\n\nKinds the caller's role may not read are **absent rather than empty**, and absent in exactly\nthe way a kind with no matches is. A confined member searching for a monitor in a workspace\nthey cannot see gets the same answer as if it did not exist, which is the same rule the rest\nof the API follows: a cross-tenant reference is \"not found\", never \"forbidden\", so a response\ncannot be used to probe for ids.",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHit"
            }
          }
        }
      },
      "WorkspaceSummary": {
        "type": "object",
        "description": "A workspace.\n\nOn an agency account these are clients, which is why a confined member is narrowed to theirs\nand why `POST /api/v1/monitors` refuses to guess one: putting a monitor somewhere the caller\ndid not name means a client seeing another client's monitor on their status page.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean",
            "description": "The one workspace every organisation has. It cannot be deleted."
          },
          "clientReference": {
            "type": "string",
            "description": "The agency's own label for the client — an account number, a contract reference. Returned\nbecause it is the agency's data and the key is the agency's, and never rendered on\nanything a client sees."
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "isDefault"
        ]
      },
      "Member": {
        "type": "object",
        "description": "Somebody with access to the organisation.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The **membership** id, not the user id: a person can hold memberships in several\norganisations and only this one is ours to change. Every other member endpoint takes this\nvalue."
          },
          "userId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "workspaceIds": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StringList"
              }
            ],
            "description": "Absent means the whole organisation; a list is a confined member — a client user.\n\n**Absent and empty are different answers.** A member confined to no workspaces sees\nnothing at all, and a client that read the two the same way would show them everything."
          },
          "joinedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "userId",
          "name",
          "email",
          "role",
          "joinedAt"
        ]
      },
      "Invitation": {
        "type": "object",
        "description": "A seat that is already spent.\n\nThe plan meters invitations alongside members precisely so an organisation on three seats\ncannot invite thirty people, which is why they are returned together.",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "workspaceIds": {
            "$ref": "#/components/schemas/StringList"
          },
          "expiresAt": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "email",
          "role",
          "expiresAt",
          "createdAt"
        ]
      },
      "Team": {
        "type": "object",
        "description": "The team, in one response.\n\nThe plan meters invitations alongside members precisely so an organisation on three seats\ncannot invite thirty people, which is why a client is given both together rather than being\nleft to fetch them separately and add them up.",
        "properties": {
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            }
          },
          "invitations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invitation"
            }
          }
        }
      },
      "ListMembersResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Team"
          }
        },
        "required": [
          "data"
        ]
      },
      "ListWorkspacesResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorkspaceSummary"
            }
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "description": "An API key, never its token.\n\nThe token is stored only as a SHA-256 and no endpoint reads one back. `prefix` is what makes\na key identifiable in a list without revealing it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "scopes": {
            "allOf": [
              {
                "$ref": "#/components/schemas/StringList"
              }
            ],
            "description": "Absent means \"everything the owner's role allows\", which is the common case."
          },
          "workspaceIds": {
            "$ref": "#/components/schemas/StringList"
          },
          "owner": {
            "allOf": [
              {
                "$ref": "#/components/schemas/KeyOwner"
              }
            ],
            "description": "Whose role this key acts with.\n\nA key inherits its creator's membership, so this is what answers \"which of these could\nremove a member\" — and what makes an orphaned key impossible, because the membership going\ntakes the key with it."
          },
          "lastUsedAt": {
            "type": "string",
            "description": "Absent on a key nobody has used. That is the one worth revoking."
          },
          "expiresAt": {
            "type": "string"
          },
          "revokedAt": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "owner",
          "createdAt"
        ]
      },
      "KeyOwner": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "label",
          "role"
        ]
      },
      "ListApiKeysResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKey"
            }
          }
        }
      },
      "Channel": {
        "type": "object",
        "description": "A notification channel.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "workspace": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Workspace"
              }
            ],
            "description": "Absent for a channel that covers the whole organisation."
          },
          "owner": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ChannelOwner"
              }
            ],
            "description": "**Never omitted.** Whoever routes an alert is usually not the person it wakes, and a list\nthat does not say which channels are somebody's own handset is one people misread — then\ndelete, and wonder why a colleague stopped being paged."
          },
          "config": {
            "type": "string",
            "description": "The channel's own configuration, minus anything sealed, as a JSON object.\n\nA string holding JSON rather than a message per kind. The shape differs by kind and a\ncustomer may hold a kind this build has never heard of during a rolling deploy, so a\nclosed message would silently drop fields a newer server sent.\n\n`google.protobuf.Struct` is the usual answer for an open shape and is the wrong one here:\nits JSON encoding is `{\"fields\":{...}}` and this API has always served the bare object.\nThe JSON body carries it as an object; a protobuf client parses this one field as JSON."
          },
          "secretSet": {
            "type": "boolean",
            "description": "Whether a signing secret or credential is stored, never what it is."
          },
          "verifiedAt": {
            "type": "string",
            "description": "A send *succeeded* — deliverability, not consent."
          },
          "confirmedAt": {
            "type": "string",
            "description": "The recipient said yes. A different question from `verified_at`, and reusing that one\nwould have made a successful test send grant permission to send."
          },
          "lastSentAt": {
            "type": "string"
          },
          "lastError": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "kind",
          "name",
          "enabled",
          "owner",
          "config",
          "secretSet",
          "createdAt"
        ]
      },
      "ChannelOwner": {
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "description": "`personal` — one person's handset or private chat — or `shared`, the team's."
          },
          "userId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        },
        "required": [
          "scope"
        ]
      },
      "ListChannelsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Channel"
            }
          }
        }
      },
      "TestDelivery": {
        "type": "object",
        "description": "What a test send did.\n\n**A failed delivery is a `200` with `delivered: false`**, not an error status. The request\nsucceeded: we asked the channel to take a message and it refused, which is exactly the answer\nthe caller wanted. Reporting it as a 4xx or 5xx would make a client retry a test that is\ntelling it the truth, and would hide the provider's own words behind a status that says\nnothing about them.",
        "properties": {
          "id": {
            "type": "string"
          },
          "delivered": {
            "type": "boolean"
          },
          "error": {
            "type": "string",
            "description": "The provider's own words, or null when it worked. Null rather than an empty string: a client\nrendering \"the channel said: \" with nothing after it is worse than rendering nothing."
          }
        },
        "required": [
          "id",
          "delivered"
        ]
      },
      "TestDeliveryResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/TestDelivery"
          }
        },
        "required": [
          "data"
        ]
      },
      "UpdatedWorkspaceResponse": {
        "type": "object",
        "description": "`PATCH /api/v1/workspaces/{id}` — the workspace as it now reads. The slug is never changed.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/WorkspaceSummary"
          }
        },
        "required": [
          "data"
        ]
      },
      "ChannelConfirmation": {
        "type": "object",
        "description": "`POST /api/v1/channels/{id}/confirmation`",
        "properties": {
          "id": {
            "type": "string"
          },
          "sent": {
            "type": "boolean",
            "description": "False when there was nothing to send: the address already said yes, or the kind never asks.\nThat is an answer, not a refusal."
          }
        },
        "required": [
          "id",
          "sent"
        ]
      },
      "ChannelConfirmationResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ChannelConfirmation"
          }
        },
        "required": [
          "data"
        ]
      },
      "SmsEnrolment": {
        "type": "object",
        "description": "`POST /api/v1/sms/enrolment` — a number stored unconfirmed, and a code on its way to it.",
        "properties": {
          "channelId": {
            "type": "string"
          },
          "pendingConfirmation": {
            "type": "boolean",
            "description": "Always true here. Said plainly so a client never shows the number as receiving alerts before\nits owner has typed the code back."
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 of the number itself. The recipient's country, not anything of ours."
          },
          "caveat": {
            "type": "string",
            "description": "What to say before relying on it: `sender-replaced`, `registration-pending` or `unverified`.\nA code rather than a sentence, because the wording has to be translated. Null when delivery\nthere is confirmed."
          }
        },
        "required": [
          "channelId",
          "pendingConfirmation",
          "country"
        ]
      },
      "SmsEnrolmentResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SmsEnrolment"
          }
        },
        "required": [
          "data"
        ]
      },
      "SmsConfirmed": {
        "type": "object",
        "description": "`POST /api/v1/sms/enrolment/confirm`",
        "properties": {
          "channelId": {
            "type": "string"
          },
          "confirmed": {
            "type": "boolean"
          }
        },
        "required": [
          "channelId",
          "confirmed"
        ]
      },
      "SmsConfirmedResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SmsConfirmed"
          }
        },
        "required": [
          "data"
        ]
      },
      "StatusPage": {
        "type": "object",
        "description": "A status page, as the list returns it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "visibility": {
            "type": "string",
            "description": "`public`, `password` or `private`."
          },
          "customDomain": {
            "type": "string"
          },
          "domainVerifiedAt": {
            "type": "string",
            "description": "Absent until the CNAME has actually been seen. Until then the slug host still serves."
          },
          "subscribersEnabled": {
            "type": "boolean"
          },
          "componentCount": {
            "type": "integer",
            "format": "int32"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "workspace",
          "visibility",
          "subscribersEnabled",
          "componentCount",
          "createdAt"
        ]
      },
      "ListStatusPagesResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatusPage"
            }
          }
        }
      },
      "StatusPageDetail": {
        "type": "object",
        "description": "One page with everything on it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "workspaceId": {
            "type": "string"
          },
          "visibility": {
            "type": "string"
          },
          "passwordSet": {
            "type": "boolean",
            "description": "Whether a password is set, **never the hash**."
          },
          "customDomain": {
            "type": "string"
          },
          "domainVerifiedAt": {
            "type": "string"
          },
          "domainVerificationToken": {
            "type": "string"
          },
          "headline": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "logoUrl": {
            "type": "string"
          },
          "faviconUrl": {
            "type": "string"
          },
          "themeAccent": {
            "type": "string"
          },
          "themeMode": {
            "type": "string"
          },
          "customCss": {
            "type": "string"
          },
          "hideVitrinaBranding": {
            "type": "boolean"
          },
          "historyDays": {
            "type": "integer",
            "format": "int32",
            "description": "Ninety days of daily buckets is the most any page shows."
          },
          "showResponseTimes": {
            "type": "boolean"
          },
          "showIncidentHistory": {
            "type": "boolean"
          },
          "subscribersEnabled": {
            "type": "boolean"
          },
          "links": {
            "type": "string",
            "description": "The customer's own links, as a JSON array. Free-form on purpose: it is their page."
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Component"
            }
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ComponentGroup"
            }
          },
          "groupByHost": {
            "type": "boolean",
            "description": "Whether the public page groups its components by the machine each one runs on.\n\nFalse by default and on every page that has not asked for it. Turning it on publishes host\nnames — the customer's own infrastructure — so it is a choice they make rather than a\nconsequence of having recorded a host."
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "workspaceId",
          "visibility",
          "passwordSet",
          "themeAccent",
          "themeMode",
          "hideVitrinaBranding",
          "historyDays",
          "showResponseTimes",
          "showIncidentHistory",
          "subscribersEnabled",
          "links",
          "createdAt",
          "updatedAt",
          "groupByHost"
        ]
      },
      "Component": {
        "type": "object",
        "description": "A monitor published on a page.\n\nAdding one is the act of publishing a monitor, which is why the check is that the monitor is\nin *the page's workspace* rather than merely in the organisation: otherwise an agency can\npublish one client's monitor on another client's page.",
        "properties": {
          "id": {
            "type": "string"
          },
          "monitorId": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "groupId": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "format": "int32"
          },
          "showUptimeBar": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "monitorId",
          "displayName",
          "position",
          "showUptimeBar"
        ]
      },
      "ComponentGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "format": "int32"
          },
          "collapsed": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "position",
          "collapsed"
        ]
      },
      "GetStatusPageResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/StatusPageDetail"
          }
        },
        "required": [
          "data"
        ]
      },
      "Ack": {
        "type": "object",
        "description": "What a write answers with when there is nothing to return but the row it touched.\n\nOne message rather than one per verb, because they differ only in which past participle they\ncarry and a client branching on the shape would be branching on grammar. **Exactly one of the\nverb fields is set**, and the rest are absent rather than false — `{\"id\": \"...\", \"deleted\":\ntrue}` is the whole body, and a body listing every verb it did not do would invite a client to\nread `\"revoked\": false` as a fact about the row.",
        "properties": {
          "id": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean"
          },
          "revoked": {
            "type": "boolean"
          },
          "removed": {
            "type": "boolean"
          },
          "cancelled": {
            "type": "boolean"
          },
          "retired": {
            "type": "boolean"
          },
          "invited": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean",
            "description": "The new state, not a verb: a channel is enabled *or* disabled by the same call, so this one\ncarries which."
          },
          "role": {
            "type": "string",
            "description": "The role a membership now holds."
          }
        },
        "required": [
          "id"
        ]
      },
      "AckResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Ack"
          }
        },
        "required": [
          "data"
        ]
      },
      "Created": {
        "type": "object",
        "description": "A row that now exists. `201`, with the id and nothing else — the caller reads it back if it\nwants the rest, rather than being handed a copy that is stale the moment a second client\nwrites.",
        "properties": {
          "id": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ]
      },
      "CreatedResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Created"
          }
        },
        "required": [
          "data"
        ]
      },
      "CreatedApiKey": {
        "type": "object",
        "description": "A key, and its token exactly once.\n\nOnly the SHA-256 is stored and no endpoint reads one back — a lost token is re-issued rather\nthan recovered.",
        "properties": {
          "id": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "The displayable prefix, which is what makes a key identifiable in a list afterwards."
          },
          "token": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "prefix",
          "token"
        ]
      },
      "CreatedApiKeyResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CreatedApiKey"
          }
        },
        "required": [
          "data"
        ]
      },
      "EnrolledAgent": {
        "type": "object",
        "description": "An enrolled agent, and its token exactly once. Same storage rule as an API key.",
        "properties": {
          "id": {
            "type": "string"
          },
          "token": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "token"
        ]
      },
      "EnrolledAgentResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/EnrolledAgent"
          }
        },
        "required": [
          "data"
        ]
      },
      "CreatedChannel": {
        "type": "object",
        "description": "A channel that now exists.",
        "properties": {
          "id": {
            "type": "string"
          },
          "secret": {
            "type": "string",
            "description": "The signing secret, returned **exactly once**, here, and only for the kinds that have one.\n\nIt is sealed the moment it is stored and there is no endpoint that reads it back — the\nreceiver needs it to verify our signature, which is the whole reason a generic webhook is\ntrustworthy at the far end."
          },
          "pendingConfirmation": {
            "type": "boolean",
            "description": "Whether the destination still has to say yes, said plainly rather than left to be inferred\nfrom the absence of a field. A client that assumed a new channel was live would show an\nalert route as working when nothing would be delivered through it until somebody clicked a\nlink in their inbox."
          }
        },
        "required": [
          "id",
          "pendingConfirmation"
        ]
      },
      "CreatedChannelResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CreatedChannel"
          }
        },
        "required": [
          "data"
        ]
      },
      "UpdatedMonitor": {
        "type": "object",
        "description": "What a monitor looks like after a `PATCH`. The four fields a caller might have changed, so a\nclient can reconcile without a second read.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "paused": {
            "type": "boolean"
          },
          "intervalSeconds": {
            "type": "integer",
            "format": "int32"
          }
        },
        "required": [
          "id",
          "name",
          "enabled",
          "paused",
          "intervalSeconds"
        ]
      },
      "UpdatedMonitorResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UpdatedMonitor"
          }
        },
        "required": [
          "data"
        ]
      },
      "UpdatedStatusPage": {
        "type": "object",
        "description": "The same, for a status page.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "visibility": {
            "type": "string"
          },
          "passwordSet": {
            "type": "boolean",
            "description": "Whether a password is set, **never the hash**."
          },
          "historyDays": {
            "type": "integer",
            "format": "int32"
          },
          "subscribersEnabled": {
            "type": "boolean"
          },
          "hideVitrinaBranding": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "visibility",
          "passwordSet",
          "historyDays",
          "subscribersEnabled",
          "hideVitrinaBranding"
        ]
      },
      "UpdatedStatusPageResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/UpdatedStatusPage"
          }
        },
        "required": [
          "data"
        ]
      },
      "ComponentAck": {
        "type": "object",
        "description": "A component published on a page, or removed from one.\n\nKeyed on the **component**, never the monitor: the same monitor can be published twice on one\npage under two names, and removing \"the monitor\" would be ambiguous about which.",
        "properties": {
          "statusPageId": {
            "type": "string",
            "description": "Set when one was added, so the caller knows which page it landed on. The removal answers\nwith the component alone — the caller named the page in the URL."
          },
          "componentId": {
            "type": "string",
            "description": "Null when the monitor was already on the page: the unique index made the request a no-op,\nwhich is a success, because the caller asked for a state that holds."
          },
          "removed": {
            "type": "boolean"
          }
        }
      },
      "ComponentAckResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/ComponentAck"
          }
        },
        "required": [
          "data"
        ]
      }
    }
  }
}
