openapi: 3.1.0
info:
  title: Pingerer API
  version: "1"
  summary: Manage sites, monitors, incidents, heartbeats, alerts, status pages and maintenance, and read check results.
  description: |
    Every request needs an API token, sent as `Authorization: Bearer <token>`.
    Create one on the API tokens page; a token can be read only, and can be
    limited to one account.

    Requests are for one account: the `Pingerer-Account` header (an account id
    like `acct_...`, or its number) or the `account_id` parameter picks it.
    Without either, it's the token's account, else your first team account,
    else your personal one. An account paused by Pingerer answers
    `403 account_suspended`.

    Responses are `{"data": ..., "meta": {...}}`. Lists are paginated with
    `page` and `per_page` (up to 100, 20 by default), and `meta` has `page`,
    `per_page`, `total`, `total_pages`, `next_page` and `prev_page`. Errors are
    `{"error": "code", "message": "...", "details": {...}}`: `bad_request`
    (400), `unauthorized` (401), `forbidden`, `read_only_token`,
    `account_suspended` and `password_change_required` (403), `not_found`
    (404), `validation_failed` and `plan_limit` (422), `rate_limited` (429).
    Each token may make 300 requests a minute.

    Ids are stable prefixed ids (`site_...`, `mon_...`). PUT and PATCH
    change only the fields sent (except a monitor's settings, which PUT
    replaces), so sending the same request twice changes nothing more. The
    Terraform provider and the GitHub Action are built on these endpoints.

    Durations (`every`, `grace`, `since`, `duration`) take seconds or `30s`, `5m`, `1h`,
    `1d`, `1w`. Times are ISO 8601.
servers:
  - url: https://pingerer.com
security:
  - bearerAuth: []
tags:
  - name: Account
  - name: Sites
  - name: Monitors
  - name: Results
  - name: Incidents
  - name: Heartbeats
  - name: Alerts
  - name: Regions
  - name: Status pages
  - name: Maintenance
  - name: Reports
  - name: Clients
paths:
  /api/v1/me:
    get:
      tags: [Account]
      operationId: getMe
      summary: The person the token belongs to
      description: Works for any account, even a paused one. The CLI's `login` checks a token with it.
      responses:
        "200":
          description: The person
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: {type: integer}
                  name: {type: string}
                  avatar_url: {type: string}
        "401": {$ref: "#/components/responses/Unauthorized"}
  /api/v1/accounts:
    get:
      tags: [Account]
      operationId: listAccounts
      summary: Your accounts
      description: Every account you belong to (a plain array). Use an id in the Pingerer-Account header.
      responses:
        "200":
          description: Accounts
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id: {type: integer}
                    name: {type: string}
                    personal: {type: boolean}
  /api/v1/push_devices:
    get:
      tags: [Account]
      operationId: listPushDevices
      summary: Your devices for push alerts
      description: The phones and tablets the Pingerer app registered for you (docs/mobile-apps.md).
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: Devices
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/PushDevice"}}
                  meta: {type: object}
        "401": {$ref: "#/components/responses/Unauthorized"}
    post:
      tags: [Account]
      operationId: registerPushDevice
      summary: Register a device for push alerts
      description: |
        What the apps call on each launch. Registering a token again
        updates its device. A token registered by someone else moves to
        you only with that install's `install_id` (a random id the app
        makes once per install), else 409. `environment` is `sandbox` for
        iOS debug builds only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, platform]
              properties:
                token: {type: string}
                platform: {type: string, enum: [ios, android]}
                install_id: {type: string, minLength: 16, maxLength: 200}
                name: {type: string}
                app_version: {type: string}
                environment: {type: string, enum: [production, sandbox]}
            example: {token: "a1b2c3…", platform: ios, install_id: "4f1c9a7e-2b3d-4e5f-8a9b-0c1d2e3f4a5b", name: "Sam's iPhone", app_version: "1.4"}
      responses:
        "201":
          description: The device
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/PushDevice"}
                  meta: {type: object, properties: {push_configured: {type: boolean}}}
        "409": {description: "The token is registered to someone else and install_id isn't that install's"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/push_devices/{id}:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, description: The device's id or its token, example: dev_3Lm9}
    delete:
      tags: [Account]
      operationId: deletePushDevice
      summary: Remove a device (on sign-out)
      responses:
        "204":
          description: Removed
        "404": {$ref: "#/components/responses/NotFound"}
  /api/v1/status:
    get:
      tags: [Account]
      operationId: getStatus
      summary: The account at a glance
      description: Overall status (down, degraded, up or pending), sites and monitors counted by status, down sites and unresolved incidents.
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
      responses:
        "200":
          description: Status
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Status"}
                  meta: {type: object}
        "403": {$ref: "#/components/responses/Forbidden"}
  /api/v1/sites:
    get:
      tags: [Sites]
      operationId: listSites
      summary: List sites
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: status, in: query, schema: {$ref: "#/components/schemas/StatusValue"}}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of sites
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Site"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Sites]
      operationId: createSite
      summary: Add a site
      description: |
        Adds the site and a monitor for each of `kinds` (default http, ssl,
        domain, dns, email_auth). `every` sets how often the uptime kinds
        (http, tcp, ping, smtp) check; the plan's shortest by default. `detect: true`
        looks the site up first to fill in each check's settings.
        `create_monitors: false` adds the site alone (Terraform adds each
        monitor itself), and `paused: true` adds it paused.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: {type: string, example: "yourbakery.com"}
                name: {type: string}
                kinds: {type: array, items: {type: string, enum: [http, tcp, ping, ssl, domain, dns, email_auth]}}
                every: {type: string, example: "1m"}
                alert_email: {type: string, format: email}
                detect: {type: boolean}
                create_monitors: {type: boolean, default: true}
                paused: {type: boolean, default: false}
            example: {url: "yourbakery.com", kinds: [http, ssl, domain], every: "1m"}
      responses:
        "201":
          description: The site, with its monitors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Site"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/sites/detect:
    post:
      tags: [Sites]
      operationId: detectSite
      summary: Look a site up before adding it
      description: What Pingerer finds for an address (certificate, domain, DNS, email) and the checks it suggests.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: {type: string}
            example: {url: "yourbakery.com"}
      responses:
        "200":
          description: The detection
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: object}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/sites/{id}:
    parameters:
      - $ref: "#/components/parameters/SiteId"
    get:
      tags: [Sites]
      operationId: getSite
      summary: A site and its monitors
      responses:
        "200":
          description: The site
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Site"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Sites]
      operationId: updateSite
      summary: Rename, pause or resume a site
      description: "`paused` pauses or resumes it, like the pause and resume endpoints. PUT works the same, and sending the same body twice changes nothing."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                paused: {type: boolean}
            example: {name: "Your Bakery", paused: false}
      responses:
        "200":
          description: The site
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Sites]
      operationId: deleteSite
      summary: Delete a site, its monitors, results and incidents
      responses:
        "204":
          description: Deleted
  /api/v1/sites/{id}/pause:
    parameters:
      - $ref: "#/components/parameters/SiteId"
    post:
      tags: [Sites]
      operationId: pauseSite
      summary: Pause a site and its monitors
      responses:
        "200":
          description: The site
  /api/v1/sites/{id}/resume:
    parameters:
      - $ref: "#/components/parameters/SiteId"
    post:
      tags: [Sites]
      operationId: resumeSite
      summary: Resume a site and the monitors pausing it stopped
      responses:
        "200":
          description: The site
  /api/v1/sites/{id}/uptime:
    parameters:
      - $ref: "#/components/parameters/SiteId"
    get:
      tags: [Sites]
      operationId: getSiteUptime
      summary: Uptime of a site's uptime monitors over 24h, 7d, 30d and 90d
      responses:
        "200":
          description: Uptime
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Uptime"}
  /api/v1/monitors:
    get:
      tags: [Monitors]
      operationId: listMonitors
      summary: List monitors
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: site_id, in: query, schema: {type: string}, description: A site's id or host}
        - {name: kind, in: query, schema: {$ref: "#/components/schemas/Kind"}}
        - {name: status, in: query, schema: {$ref: "#/components/schemas/StatusValue"}}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of monitors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Monitor"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Monitors]
      operationId: createMonitor
      summary: Add a monitor to a site
      description: "`settings` are the kind's (for http: url, http_method, expected_statuses, keyword, keyword_absent, max_response_ms, follow_redirects, headers). A multi-step check (kind steps, paid plans) takes `settings.steps` (up to 10 of name, method, url, headers, body_type none/form/json, body, follow_redirects, assertions [{type, field, value}] and extract [{name, from, field}]) and `secrets`, its secret variables (write-only, always shown masked)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [site_id, kind]
              properties:
                site_id: {type: string}
                kind: {$ref: "#/components/schemas/Kind"}
                name: {type: string}
                settings: {type: object}
                every: {type: string}
                timeout_seconds: {type: integer}
                regions: {type: array, items: {type: string}, description: Region codes (GET /api/v1/regions)}
                confirm_regions: {type: integer}
                secrets: {type: object, additionalProperties: {type: string}, description: "Multi-step checks only: {name: value}, used in steps as {{name}}."}
                paused: {type: boolean, default: false}
            example: {site_id: "yourbakery.com", kind: tcp, name: "Port 22", settings: {host: "yourbakery.com", port: 22}}
      responses:
        "201":
          description: The monitor
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/monitors/{id}:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    get:
      tags: [Monitors]
      operationId: getMonitor
      summary: A monitor
      responses:
        "200":
          description: The monitor
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Monitor"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Monitors]
      operationId: updateMonitor
      summary: Change a monitor
      description: |
        Settings given are merged into the current ones. With PUT instead,
        they replace them: settings left out go back to their defaults.
        `paused` pauses or resumes it. Fields left out stay as they are.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                settings: {type: object}
                every: {type: string}
                timeout_seconds: {type: integer}
                regions: {type: array, items: {type: string}}
                confirm_regions: {type: integer}
                secrets: {type: object, additionalProperties: {type: [string, "null"]}, description: "Multi-step checks only: merged into the saved ones; a masked value keeps one, null removes it."}
                paused: {type: boolean}
            example: {every: "5m", settings: {keyword: "Open today"}}
      responses:
        "200":
          description: The monitor
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Monitors]
      operationId: deleteMonitor
      summary: Delete a monitor and its results
      responses:
        "204":
          description: Deleted
  /api/v1/monitors/{id}/pause:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    post:
      tags: [Monitors]
      operationId: pauseMonitor
      summary: Pause a monitor
      responses:
        "200":
          description: The monitor
  /api/v1/monitors/{id}/resume:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    post:
      tags: [Monitors]
      operationId: resumeMonitor
      summary: Resume a monitor
      description: A plan with no room for another active monitor answers 422 plan_limit.
      responses:
        "200":
          description: The monitor
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/monitors/{id}/test:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    post:
      tags: [Monitors]
      operationId: testMonitor
      summary: Run one check now
      description: Checks once from `region` (default the monitor's first) and returns the result without recording it.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                region: {type: string, example: yyz}
      responses:
        "200":
          description: The result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/CheckResult"}
  /api/v1/monitors/{id}/uptime:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    get:
      tags: [Monitors]
      operationId: getMonitorUptime
      summary: Uptime of a monitor over 24h, 7d, 30d and 90d
      responses:
        "200":
          description: Uptime
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Uptime"}
  /api/v1/monitors/{monitor_id}/results:
    parameters:
      - {name: monitor_id, in: path, required: true, schema: {type: string}, example: mon_8xKq2}
    get:
      tags: [Results]
      operationId: listResults
      summary: A monitor's check results, newest first
      parameters:
        - {name: since, in: query, schema: {type: string}, example: 24h}
        - {name: until, in: query, schema: {type: string, format: date-time}}
        - {name: region, in: query, schema: {type: string}}
        - {name: ok, in: query, schema: {type: boolean}}
        - $ref: "#/components/parameters/Page"
        - {name: per_page, in: query, schema: {type: integer, maximum: 500}}
      responses:
        "200":
          description: A page of results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/CheckResult"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
  /api/v1/incidents:
    get:
      tags: [Incidents]
      operationId: listIncidents
      summary: List incidents, newest first
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: state, in: query, schema: {type: string, enum: [open, acknowledged, resolved, unresolved]}}
        - {name: kind, in: query, schema: {type: string, enum: [down, degraded, ssl_expiring, domain_expiring, dns_changed, email_auth, heartbeat_missed]}}
        - {name: site_id, in: query, schema: {type: string}}
        - {name: monitor_id, in: query, schema: {type: string}}
        - {name: since, in: query, schema: {type: string}, example: 7d}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of incidents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Incident"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
  /api/v1/incidents/{id}:
    parameters:
      - $ref: "#/components/parameters/IncidentId"
    get:
      tags: [Incidents]
      operationId: getIncident
      summary: An incident and its timeline
      responses:
        "200":
          description: The incident
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Incident"}
        "404": {$ref: "#/components/responses/NotFound"}
  /api/v1/incidents/{id}/acknowledge:
    parameters:
      - $ref: "#/components/parameters/IncidentId"
    post:
      tags: [Incidents]
      operationId: acknowledgeIncident
      summary: Acknowledge an incident
      description: Stops escalation. Any member may acknowledge.
      responses:
        "200":
          description: The incident
  /api/v1/incidents/{id}/notes:
    parameters:
      - $ref: "#/components/parameters/IncidentId"
    post:
      tags: [Incidents]
      operationId: addIncidentNote
      summary: Add a note to an incident's timeline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message: {type: string}
            example: {message: "Restarted the web server"}
      responses:
        "201":
          description: The incident
  /api/v1/incidents/{id}/resolve:
    parameters:
      - $ref: "#/components/parameters/IncidentId"
    post:
      tags: [Incidents]
      operationId: resolveIncident
      summary: Resolve an incident
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                message: {type: string}
            example: {message: "Fixed by the host"}
      responses:
        "200":
          description: The incident
  /api/v1/heartbeats:
    get:
      tags: [Heartbeats]
      operationId: listHeartbeats
      summary: List heartbeats
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of heartbeats
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Heartbeat"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Heartbeats]
      operationId: createHeartbeat
      summary: Add a heartbeat
      description: Your job requests the returned `ping_url` each time it runs. No ping within `every` plus `grace` opens an incident. With `email_pings` (where Pingerer has turned on email heartbeats for the account), an email to `ping_email` counts as a ping too; a subject with "failed" in it reports a failure.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
                every: {type: string, example: 1d}
                grace: {type: string, example: 30m}
                email_pings: {type: boolean, default: false}
            example: {name: "Nightly backup", every: "1d", grace: "30m"}
      responses:
        "201":
          description: The heartbeat
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/heartbeats/{id}:
    parameters:
      - $ref: "#/components/parameters/MonitorId"
    get:
      tags: [Heartbeats]
      operationId: getHeartbeat
      summary: A heartbeat
      responses:
        "200":
          description: The heartbeat
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Heartbeat"}
    patch:
      tags: [Heartbeats]
      operationId: updateHeartbeat
      summary: Change a heartbeat
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                every: {type: string}
                grace: {type: string}
                email_pings: {type: boolean}
            example: {every: "12h"}
      responses:
        "200":
          description: The heartbeat
    delete:
      tags: [Heartbeats]
      operationId: deleteHeartbeat
      summary: Delete a heartbeat
      responses:
        "204":
          description: Deleted
  /api/v1/alert_channels:
    get:
      tags: [Alerts]
      operationId: listAlertChannels
      summary: List alert channels and their rules
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: kind, in: query, schema: {type: string}}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of channels
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/AlertChannel"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Alerts]
      operationId: createAlertChannel
      summary: Add an alert channel
      description: |
        Settings by kind: email `addresses`; slack `webhook_url` and `channel`;
        discord and teams `webhook_url`; telegram `chat_id`; webhook `url`
        (its signing secret is in this response only); sms `numbers`; push
        `user_ids` (members' ids or emails; none means everyone with the
        app) and `critical`, once Pingerer's push is set up.
        `site_ids` limits it to some sites (default every site), and `events`
        to some events (default all); `default_rule: false` adds no rule, for
        adding each one with POST /api/v1/alert_rules. Email to an address
        that isn't a member's is sent a confirmation link first.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind]
              properties:
                kind: {type: string, enum: [email, slack, discord, telegram, teams, webhook, sms, push]}
                name: {type: string}
                addresses: {type: array, items: {type: string}}
                webhook_url: {type: string}
                channel: {type: string}
                url: {type: string}
                chat_id: {type: string}
                numbers: {type: array, items: {type: string}}
                user_ids: {type: array, items: {type: string}}
                critical: {type: boolean}
                site_ids: {type: array, items: {type: string}}
                events: {type: array, items: {type: string, enum: [down, recovered, degraded, expiring, dns_changed, email_auth, heartbeat_missed]}}
                enabled: {type: boolean, default: true}
                default_rule: {type: boolean, default: true}
            example: {kind: webhook, name: "Ops webhook", url: "https://ops.example.com/pingerer"}
      responses:
        "201":
          description: The channel
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/alert_channels/{id}:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, example: ach_3Lm9}
    get:
      tags: [Alerts]
      operationId: getAlertChannel
      summary: An alert channel
      responses:
        "200":
          description: The channel
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/AlertChannel"}
    patch:
      tags: [Alerts]
      operationId: updateAlertChannel
      summary: Change an alert channel
      description: |
        Changes `name`, `enabled` and the kind's settings that are sent (the
        same as when adding one); the rest stay as they are, so secrets
        needn't be sent again. A new destination needs verifying again. The
        kind can't change. PUT works the same.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                enabled: {type: boolean}
                addresses: {type: array, items: {type: string}}
                webhook_url: {type: string}
                channel: {type: string}
                url: {type: string}
                chat_id: {type: string}
                numbers: {type: array, items: {type: string}}
            example: {name: "Ops team", enabled: true}
      responses:
        "200":
          description: The channel
        "403": {$ref: "#/components/responses/Forbidden"}
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Alerts]
      operationId: deleteAlertChannel
      summary: Delete an alert channel
      responses:
        "204":
          description: Deleted
  /api/v1/alert_rules:
    get:
      tags: [Alerts]
      operationId: listAlertRules
      summary: List alert rules
      description: Which sites' events go to which channel. A rule with no site covers every site, new ones too.
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: alert_channel_id, in: query, schema: {type: string}}
        - {name: site_id, in: query, schema: {type: string}, description: A site's id or host}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of rules
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/AlertRule"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Alerts]
      operationId: createAlertRule
      summary: Send a site's events to a channel
      description: |
        `site_id` (an id or host; leave it out for every site), `events`
        (default all) and `escalate_after_minutes` (alert this channel only
        if an incident is still unacknowledged that long). A channel has one
        rule per site.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [alert_channel_id]
              properties:
                alert_channel_id: {type: string}
                site_id: {type: [string, "null"]}
                events: {type: array, items: {type: string, enum: [down, recovered, degraded, expiring, dns_changed, email_auth, heartbeat_missed]}}
                escalate_after_minutes: {type: [integer, "null"]}
            example: {alert_channel_id: ach_3Lm9, site_id: "yourbakery.com", events: [down, recovered]}
      responses:
        "201":
          description: The rule
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/AlertRule"}
        "404": {$ref: "#/components/responses/NotFound"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/alert_rules/{id}:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, example: arule_7Qp2}
    get:
      tags: [Alerts]
      operationId: getAlertRule
      summary: An alert rule
      responses:
        "200":
          description: The rule
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/AlertRule"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Alerts]
      operationId: updateAlertRule
      summary: Change an alert rule
      description: Changes the fields sent (`site_id` null for every site); the channel can't change. PUT works the same.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                site_id: {type: [string, "null"]}
                events: {type: array, items: {type: string}}
                escalate_after_minutes: {type: [integer, "null"]}
            example: {events: [down], escalate_after_minutes: 15}
      responses:
        "200":
          description: The rule
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Alerts]
      operationId: deleteAlertRule
      summary: Delete an alert rule
      responses:
        "204":
          description: Deleted
  /api/v1/regions:
    get:
      tags: [Regions]
      operationId: listRegions
      summary: Where checks run from
      description: |
        Every region, home first. `meta.max_per_monitor` is how many a
        monitor may use on the account's plan (null for all), `meta.default`
        the regions a new monitor gets, and `meta.min_interval_seconds` the
        plan's shortest interval.
      responses:
        "200":
          description: Regions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        code: {type: string, example: fsn}
                        name: {type: string, example: Falkenstein}
                        home: {type: boolean}
                  meta:
                    type: object
                    properties:
                      max_per_monitor: {type: [integer, "null"]}
                      default: {type: array, items: {type: string}}
                      min_interval_seconds: {type: integer}
  /api/v1/status_pages:
    get:
      tags: [Status pages]
      operationId: listStatusPages
      summary: List status pages
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of status pages
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/StatusPage"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Status pages]
      operationId: createStatusPage
      summary: Make a status page
      description: |
        Up to the plan's limit. The slug is made from the name unless given.
        It isn't public until `published` is true. A `custom_domain` is
        verified with the TXT record in `custom_domain_records`, then served
        once its CNAME points at us.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                slug: {type: string}
                description: {type: [string, "null"]}
                brand_color: {type: string, example: "#2F45E8"}
                timezone: {type: string, example: America/New_York}
                subscribe_enabled: {type: boolean}
                auto_update: {type: boolean}
                hide_powered_by: {type: boolean, description: Needs white label or the Agency plan}
                published: {type: boolean}
                custom_domain: {type: [string, "null"], description: Needs custom domains on the plan}
            example: {name: "Your Bakery", slug: "your-bakery", published: true}
      responses:
        "201":
          description: The status page
        "403": {$ref: "#/components/responses/Forbidden"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/status_pages/{id}:
    parameters:
      - {name: id, in: path, required: true, description: The page's id or slug, schema: {type: string}, example: your-bakery}
    get:
      tags: [Status pages]
      operationId: getStatusPage
      summary: A status page, its components and their status now
      responses:
        "200":
          description: The status page, with `status`, `components` and `subscribers` (confirmed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/StatusPage"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Status pages]
      operationId: updateStatusPage
      summary: Change, publish or unpublish a status page
      description: Changes the fields sent; the rest stay as they are. PUT works the same.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                slug: {type: string}
                description: {type: [string, "null"]}
                brand_color: {type: string, example: "#2F45E8"}
                timezone: {type: string, example: America/New_York}
                subscribe_enabled: {type: boolean}
                auto_update: {type: boolean}
                hide_powered_by: {type: boolean, description: Needs white label or the Agency plan}
                published: {type: boolean}
                custom_domain: {type: [string, "null"], description: Needs custom domains on the plan}
            example: {published: true, brand_color: "#2F45E8"}
      responses:
        "200":
          description: The status page
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Status pages]
      operationId: deleteStatusPage
      summary: Delete a status page, its components, updates and subscribers
      responses:
        "204":
          description: Deleted
  /api/v1/status_pages/{status_page_id}/components:
    parameters:
      - {name: status_page_id, in: path, required: true, description: The page's id or slug, schema: {type: string}, example: your-bakery}
    get:
      tags: [Status pages]
      operationId: listStatusPageComponents
      summary: A status page's components, in order
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of components
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/StatusPageComponent"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Status pages]
      operationId: createStatusPageComponent
      summary: Add a component
      description: |
        A component follows sites (their uptime monitors) and monitors
        (a heartbeat, say): `site_ids` takes ids or hosts. It goes last
        unless `position` (0 is first) says otherwise.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
                description: {type: [string, "null"]}
                display_uptime: {type: boolean, default: true}
                position: {type: integer}
                site_ids: {type: array, items: {type: string}}
                monitor_ids: {type: array, items: {type: string}}
            example: {name: "Online shop", site_ids: ["shop.yourbakery.com"]}
      responses:
        "201":
          description: The component
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/StatusPageComponent"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/status_pages/{status_page_id}/components/{id}:
    parameters:
      - {name: status_page_id, in: path, required: true, description: The page's id or slug, schema: {type: string}, example: your-bakery}
      - {name: id, in: path, required: true, schema: {type: string}, example: spc_5Wd1}
    get:
      tags: [Status pages]
      operationId: getStatusPageComponent
      summary: A component
      responses:
        "200":
          description: The component
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/StatusPageComponent"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Status pages]
      operationId: updateStatusPageComponent
      summary: Change or move a component
      description: Changes the fields sent; `site_ids` and `monitor_ids` replace what it followed. `position` moves it, and the others move along. PUT works the same.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                description: {type: [string, "null"]}
                display_uptime: {type: boolean}
                position: {type: integer}
                site_ids: {type: array, items: {type: string}}
                monitor_ids: {type: array, items: {type: string}}
            example: {position: 0}
      responses:
        "200":
          description: The component
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Status pages]
      operationId: deleteStatusPageComponent
      summary: Remove a component
      responses:
        "204":
          description: Deleted
  /api/v1/status_pages/{status_page_id}/updates:
    parameters:
      - {name: status_page_id, in: path, required: true, description: The page's id or slug, schema: {type: string}, example: your-bakery}
    get:
      tags: [Status pages]
      operationId: listStatusUpdates
      summary: A status page's updates, newest first
      parameters:
        - {name: open, in: query, description: Only the newest update of each unresolved thread, schema: {type: boolean}}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of updates
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/StatusUpdate"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Status pages]
      operationId: postStatusUpdate
      summary: Post a status update
      description: |
        Posts to the page and emails its confirmed subscribers (when the page
        is published). `components` maps a component's name or id to its
        impact (an empty string leaves it out). Pass `thread_id` to follow
        up on an earlier update: the title and components carry over, and
        `resolved` marks them operational. Admins only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [state]
              properties:
                state: {type: string, enum: [investigating, identified, monitoring, resolved, scheduled]}
                title: {type: string, description: Required unless thread_id is given}
                message: {type: string}
                components: {type: object, additionalProperties: {type: string, enum: [operational, degraded, partial_outage, major_outage, ""]}}
                thread_id: {type: string}
                incident_id: {type: string}
                scheduled_for: {type: string, format: date-time, description: For scheduled maintenance}
                scheduled_until: {type: string, format: date-time}
            example: {state: investigating, title: "Checkout errors", message: "We're looking into it.", components: {"Online shop": partial_outage}}
      responses:
        "201":
          description: The update
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/StatusUpdate"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/status_pages/{status_page_id}/updates/{id}:
    parameters:
      - {name: status_page_id, in: path, required: true, schema: {type: string}, example: your-bakery}
      - {name: id, in: path, required: true, schema: {type: string}, example: su_8Fk2}
    get:
      tags: [Status pages]
      operationId: getStatusUpdate
      summary: A status update
      responses:
        "200":
          description: The update
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/StatusUpdate"}
        "404": {$ref: "#/components/responses/NotFound"}
    patch:
      tags: [Status pages]
      operationId: updateStatusUpdate
      summary: Change an update's wording, state or components
      description: Doesn't email subscribers again.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: {type: string}
                message: {type: string}
                state: {type: string}
                components: {type: object}
      responses:
        "200":
          description: The update
  /api/v1/maintenance_windows:
    get:
      tags: [Maintenance]
      operationId: listMaintenanceWindows
      summary: List maintenance windows
      description: Newest first. `state` filters to `upcoming` (scheduled, in progress or repeating), `done` or `cancelled`.
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - {name: state, in: query, schema: {type: string, enum: [upcoming, done, cancelled]}}
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of maintenance windows
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/MaintenanceWindow"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
    post:
      tags: [Maintenance]
      operationId: createMaintenanceWindow
      summary: Schedule maintenance
      description: |
        During the window checks keep running and are stored, but the affected
        sites' monitors don't open incidents or send alerts, and the time
        doesn't count against uptime. Times without an offset are local to
        `time_zone` (the account's by default). `sites` takes ids or hosts;
        or set `all_sites`. `recurrence: weekly` repeats on `days` (sun..sat;
        the start's day by default) at the same local time.
        `notify_subscribers_before` is hours before (default 24; `false` for
        no email to status page subscribers).

        For a deploy, `duration` (like `15m`) without `starts_at` starts it
        now, and without `ends_at` ends it that long after it starts; the
        title is then "Maintenance" unless given.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                duration: {type: string, example: "15m"}
                title: {type: string}
                message: {type: string, description: Shown on your status page}
                starts_at: {type: string, format: date-time}
                ends_at: {type: string, format: date-time}
                time_zone: {type: string, example: America/Toronto}
                sites: {type: array, items: {type: string}}
                all_sites: {type: boolean}
                recurrence: {type: string, enum: [none, weekly]}
                days: {type: array, items: {type: string, enum: [sun, mon, tue, wed, thu, fri, sat]}}
                notify_subscribers_before: {type: [integer, boolean, "null"]}
            example: {title: "Database upgrade", starts_at: "2026-10-14T02:00:00-04:00", ends_at: "2026-10-14T02:30:00-04:00", sites: ["shop.yourbakery.com"]}
      responses:
        "201":
          description: The maintenance window
        "422": {$ref: "#/components/responses/Invalid"}
  /api/v1/maintenance_windows/{id}:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, example: mw_4Tq8}
    get:
      tags: [Maintenance]
      operationId: getMaintenanceWindow
      summary: A maintenance window
      responses:
        "200":
          description: The maintenance window
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/MaintenanceWindow"}
    patch:
      tags: [Maintenance]
      operationId: updateMaintenanceWindow
      summary: Change a maintenance window
      description: Takes the same fields as scheduling one; those left out stay as they are.
      responses:
        "200":
          description: The maintenance window
        "422": {$ref: "#/components/responses/Invalid"}
    delete:
      tags: [Maintenance]
      operationId: deleteMaintenanceWindow
      summary: Delete a maintenance window
      responses:
        "204":
          description: Deleted
  /api/v1/maintenance_windows/{id}/cancel:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, example: mw_4Tq8}
    post:
      tags: [Maintenance]
      operationId: cancelMaintenanceWindow
      summary: Cancel a maintenance window
      description: It stays in the list as cancelled and no longer holds anything back.
      responses:
        "200":
          description: The maintenance window
  /api/v1/maintenance_windows/{id}/end:
    parameters:
      - {name: id, in: path, required: true, schema: {type: string}, example: mw_4Tq8}
    post:
      tags: [Maintenance]
      operationId: endMaintenanceWindow
      summary: End a maintenance window now
      description: |
        One in progress ends now (a weekly one also stops repeating), so its
        history stays; one that hasn't started is cancelled; one that's over
        is left as it is. Safe to call twice.
      responses:
        "200":
          description: The maintenance window
        "404": {$ref: "#/components/responses/NotFound"}
  /api/v1/reports/{month}:
    parameters:
      - {name: month, in: path, required: true, schema: {type: string}, description: "YYYY-MM, or last for last month", example: "2026-09"}
    get:
      tags: [Reports]
      operationId: getMonthlyReport
      summary: A month's report
      description: |
        Overall uptime (every uptime monitor's checks together, so busier
        monitors weigh more), incidents, downtime, average response, a row
        per site and heartbeat against the account's SLA target, and what's
        worth fixing. Maintenance windows don't count. The month in progress
        is reported so far; a month that hasn't started is a bad request.
      responses:
        "200":
          description: The report
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/MonthlyReport"}
        "400": {description: Not a month, or one that hasn't started}
  /api/v1/clients:
    get:
      tags: [Clients]
      operationId: listClients
      summary: List an agency's client workspaces
      description: |
        The account must be an agency (the Agency plan). Each client has its
        own sites, alerts, status pages and reports, and shares the agency's
        monitor pool. To work inside a client, pass its id in the
        Pingerer-Account header.
      parameters:
        - $ref: "#/components/parameters/AccountHeader"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: A page of clients
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: "#/components/schemas/Client"}}
                  meta: {$ref: "#/components/schemas/PageMeta"}
        "404": {$ref: "#/components/responses/NotFound"}
    post:
      tags: [Clients]
      operationId: createClient
      summary: Add a client workspace
      description: Agency admins only. The agency's members can open it; invite the client's own people from the Clients page.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string}
            example: {name: "Your Bakery"}
      responses:
        "201":
          description: The client
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {$ref: "#/components/schemas/Client"}
        "403": {$ref: "#/components/responses/Forbidden"}
        "422": {$ref: "#/components/responses/Invalid"}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    AccountHeader:
      name: Pingerer-Account
      in: header
      required: false
      schema: {type: string}
      description: The account to use (id or acct_... id).
    Page:
      name: page
      in: query
      schema: {type: integer, minimum: 1}
    PerPage:
      name: per_page
      in: query
      schema: {type: integer, minimum: 1, maximum: 100}
    SiteId:
      name: id
      in: path
      required: true
      schema: {type: string}
      description: The site's id (site_...) or host.
      example: yourbakery.com
    MonitorId:
      name: id
      in: path
      required: true
      schema: {type: string}
      example: mon_8xKq2
    IncidentId:
      name: id
      in: path
      required: true
      schema: {type: string}
      description: The incident's id (inc_...), number (42) or reference (INC-0042).
      example: INC-0042
  responses:
    Unauthorized:
      description: No token, or one that isn't valid
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Forbidden:
      description: Not allowed (read only token, not an admin, or the account is paused)
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    NotFound:
      description: Not found in this account
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
    Invalid:
      description: Not valid, or over the plan's limits (plan_limit)
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
  schemas:
    Client:
      type: object
      properties:
        id: {type: string, example: acct_7Hd2k}
        name: {type: string}
        state: {type: string, enum: [up, attention, down]}
        sites: {type: integer}
        open_incidents: {type: integer}
        active_monitors: {type: integer}
        uptime_30d: {type: [number, "null"]}
        created_at: {type: string, format: date-time}
    Error:
      type: object
      required: [error, message]
      properties:
        error: {type: string, example: plan_limit}
        message: {type: string}
        details: {type: object}
    PageMeta:
      type: object
      properties:
        page: {type: integer}
        per_page: {type: integer}
        total: {type: integer}
        total_pages: {type: integer}
        next_page: {type: [integer, "null"]}
        prev_page: {type: [integer, "null"]}
    StatusValue:
      type: string
      enum: [pending, up, degraded, down, paused]
    Kind:
      type: string
      enum: [http, tcp, ping, smtp, ssl, domain, dns, email_auth, heartbeat, steps]
      description: "smtp (a mail server: settings host, port, security starttls|tls|none, verify_tls) is available where Pingerer has turned on mail server checks for the account."
    Site:
      type: object
      properties:
        id: {type: string, example: site_4xT9}
        name: {type: string}
        host: {type: string}
        url: {type: string}
        status: {$ref: "#/components/schemas/StatusValue"}
        paused: {type: boolean}
        monitors: {type: array, items: {$ref: "#/components/schemas/Monitor"}}
    Monitor:
      type: object
      properties:
        id: {type: string, example: mon_8xKq2}
        site_id: {type: [string, "null"]}
        kind: {$ref: "#/components/schemas/Kind"}
        name: {type: string}
        target: {type: [string, "null"]}
        status: {$ref: "#/components/schemas/StatusValue"}
        settings: {type: object}
        interval_seconds: {type: integer}
        timeout_seconds: {type: integer}
        regions: {type: array, items: {type: string}}
        confirm_regions: {type: integer}
        secrets: {type: object, description: "Multi-step checks only: secret variable names, values masked."}
        paused: {type: boolean}
        paused_reason: {type: [string, "null"]}
        last_checked_at: {type: [string, "null"], format: date-time}
    Heartbeat:
      allOf:
        - $ref: "#/components/schemas/Monitor"
        - type: object
          properties:
            ping_url: {type: string}
            expected_every_seconds: {type: integer}
            grace_seconds: {type: integer}
            email_pings: {type: boolean}
            ping_email: {type: [string, "null"], description: "The address to email, with email_pings on."}
            last_ping_at: {type: [string, "null"], format: date-time}
            deadline: {type: string, format: date-time}
    CheckResult:
      type: object
      properties:
        region: {type: string}
        ok: {type: boolean}
        status_code: {type: [integer, "null"]}
        latency_ms: {type: [integer, "null"]}
        error: {type: [string, "null"]}
        details: {type: object}
        checked_at: {type: string, format: date-time}
    Uptime:
      type: object
      properties:
        windows:
          type: object
          additionalProperties:
            type: object
            properties:
              checks: {type: integer}
              failures: {type: integer}
              uptime_percent: {type: [number, "null"]}
              avg_latency_ms: {type: [integer, "null"]}
    Incident:
      type: object
      properties:
        id: {type: string, example: inc_2bQ7}
        number: {type: integer}
        reference: {type: string, example: INC-0042}
        kind: {type: string}
        state: {type: string, enum: [open, acknowledged, resolved]}
        cause: {type: [string, "null"]}
        site_id: {type: [string, "null"]}
        monitor_id: {type: string}
        opened_at: {type: string, format: date-time}
        acknowledged_at: {type: [string, "null"], format: date-time}
        resolved_at: {type: [string, "null"], format: date-time}
        duration_seconds: {type: integer}
        events:
          type: array
          items:
            type: object
            properties:
              kind: {type: string}
              message: {type: [string, "null"]}
              region: {type: [string, "null"]}
              created_at: {type: string, format: date-time}
    AlertChannel:
      type: object
      properties:
        id: {type: string}
        kind: {type: string}
        name: {type: string}
        enabled: {type: boolean}
        verified: {type: boolean}
        failing: {type: boolean}
        config: {type: object, description: Settings, with secrets shown as their last 4 characters}
        rules: {type: array, items: {$ref: "#/components/schemas/AlertRule"}}
        secret: {type: string, description: "A webhook's signing secret: only in the response that adds it"}
    AlertRule:
      type: object
      properties:
        id: {type: string, example: arule_7Qp2}
        alert_channel_id: {type: string, description: Not inside a channel's rules}
        site_id: {type: [string, "null"]}
        all_sites: {type: boolean}
        events: {type: array, items: {type: string}}
        escalate_after_minutes: {type: [integer, "null"]}
    PushDevice:
      type: object
      properties:
        id: {type: string, example: dev_3Lm9}
        platform: {type: string, enum: [ios, android]}
        name: {type: string}
        app_version: {type: string}
        environment: {type: string, enum: [production, sandbox]}
        alerts_enabled: {type: boolean}
        last_seen_at: {type: string, format: date-time}
        last_pushed_at: {type: string, format: date-time}
        created_at: {type: string, format: date-time}
    Status:
      type: object
      properties:
        account: {type: object}
        status: {type: string, enum: [down, degraded, up, pending]}
        sites: {type: object, additionalProperties: {type: integer}}
        monitors: {type: object, additionalProperties: {type: integer}}
        down_sites: {type: array, items: {$ref: "#/components/schemas/Site"}}
        open_incidents: {type: array, items: {$ref: "#/components/schemas/Incident"}}
    MaintenanceWindow:
      type: object
      properties:
        id: {type: string, example: mw_4Tq8}
        title: {type: string}
        message: {type: [string, "null"]}
        state: {type: string, enum: [scheduled, in_progress, repeats_weekly, done, cancelled]}
        starts_at: {type: string, format: date-time}
        ends_at: {type: string, format: date-time}
        time_zone: {type: string}
        all_sites: {type: boolean}
        sites: {type: array, items: {type: object, properties: {id: {type: string}, host: {type: string}}}}
        recurrence: {type: string, enum: [none, weekly]}
        days: {type: array, items: {type: string}}
        notify_subscribers_before: {type: [integer, "null"]}
        next_occurrence: {type: [object, "null"], properties: {starts_at: {type: string}, ends_at: {type: string}}}
        last_occurrence: {type: [object, "null"], properties: {starts_at: {type: string}, ends_at: {type: string}, failed_checks_held_back: {type: integer}}}
        cancelled_at: {type: [string, "null"], format: date-time}
    MonthlyReport:
      type: object
      properties:
        month: {type: string, example: "2026-09"}
        label: {type: string, example: September 2026}
        starts_at: {type: string, format: date-time}
        ends_at: {type: string, format: date-time}
        finished: {type: boolean}
        sla_target: {type: number, example: 99.9}
        overall_uptime: {type: [number, "null"]}
        incidents: {type: integer}
        incidents_by_kind: {type: object}
        downtime_seconds: {type: integer}
        longest_outage: {type: [object, "null"]}
        average_response_ms: {type: [integer, "null"]}
        regions: {type: integer}
        rows:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              name: {type: string}
              type: {type: string, enum: [site, heartbeat]}
              uptime: {type: [number, "null"]}
              downtime_seconds: {type: integer}
              incidents: {type: integer}
              average_response_ms: {type: integer}
              sla_met: {type: [boolean, "null"]}
              missed_runs: {type: integer}
        worth_fixing: {type: array, items: {type: string}}
    StatusPage:
      type: object
      properties:
        id: {type: string, example: sp_3Hn5}
        name: {type: string}
        slug: {type: string}
        description: {type: [string, "null"]}
        url: {type: string}
        published: {type: boolean}
        brand_color: {type: string}
        timezone: {type: string}
        subscribe_enabled: {type: boolean}
        auto_update: {type: boolean}
        hide_powered_by: {type: boolean}
        custom_domain: {type: [string, "null"]}
        custom_domain_state: {type: string, enum: [none, verifying, pending, active, failed]}
        custom_domain_records:
          type: array
          description: The CNAME and TXT records a custom domain needs
          items:
            type: object
            properties:
              type: {type: string}
              name: {type: string}
              value: {type: string}
        status: {type: string, enum: [operational, degraded, partial_outage, major_outage], description: Only on a single page}
        components: {type: array, items: {$ref: "#/components/schemas/StatusPageComponent"}}
        subscribers: {type: integer}
    StatusPageComponent:
      type: object
      properties:
        id: {type: string, example: spc_5Wd1}
        status_page_id: {type: string}
        name: {type: string}
        description: {type: [string, "null"]}
        position: {type: integer}
        display_uptime: {type: boolean}
        status: {type: string, enum: [operational, degraded, partial_outage, major_outage]}
        site_ids: {type: array, items: {type: string}}
        monitor_ids: {type: array, items: {type: string}}
    StatusUpdate:
      type: object
      properties:
        id: {type: string, example: su_8Fk2}
        status_page_id: {type: string}
        thread_id: {type: string}
        incident_id: {type: [string, "null"]}
        title: {type: string}
        state: {type: string, enum: [investigating, identified, monitoring, resolved, scheduled]}
        body: {type: [string, "null"]}
        source: {type: string, enum: [user, system, api]}
        components:
          type: array
          items:
            type: object
            properties:
              id: {type: string}
              name: {type: string}
              impact: {type: string}
        published_at: {type: string, format: date-time}
