# Pingdeck public API v1 — OpenAPI 3.1 source of truth
# (05-api-and-integrations.md "OpenAPI 3.1 skeleton" + DECISIONS.md #13:
# base path /v1). Extends the spec's illustrative 5-path skeleton to cover
# every endpoint actually implemented in apps/web/app/v1/** + the heartbeat
# ingest aliases — additive-only new file, per this task's OWNED scope.
#
# Not yet covered here (out of this task's build list): custom-domains,
# /v1/hooks (Zapier/Make REST Hooks). Add them in the same style once built.

openapi: 3.1.0
info:
  title: Pingdeck API
  version: 1.0.0
  description: White-label uptime monitoring & client reporting for agencies.
  contact: { email: support@pingdeck.app }
servers:
  - url: https://pingdeck.app/v1
security:
  - bearerAuth: []

paths:
  /me:
    get:
      operationId: getMe
      summary: Introspect the current API key
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Agency, plan, scopes, rate limits, quota snapshot
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Me" }

  /monitors:
    get:
      operationId: listMonitors
      summary: List monitors
      parameters:
        - { name: client_id, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { $ref: "#/components/schemas/MonitorType" } }
        - { name: status, in: query, schema: { type: string, enum: [pending, up, down, paused] } }
        - { name: paused, in: query, schema: { type: boolean } }
        - {
            name: url,
            in: query,
            description: "Exact-match target filter",
            schema: { type: string },
          }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 25 },
          }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Paginated monitors
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Monitor" } }
                  next_cursor: { type: [string, "null"] }
                  has_more: { type: boolean }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createMonitor
      summary: Create a monitor
      parameters:
        - { name: Idempotency-Key, in: header, schema: { type: string, maxLength: 255 } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MonitorCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Monitor" }
        "403": { $ref: "#/components/responses/PlanLimit" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /monitors/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      operationId: getMonitor
      summary: Fetch one monitor
      responses:
        "200":
          {
            description: Monitor,
            content: { application/json: { schema: { $ref: "#/components/schemas/Monitor" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateMonitor
      summary: Update monitor config
      requestBody:
        content: { application/json: { schema: { type: object } } }
      responses:
        "200":
          {
            description: Updated,
            content: { application/json: { schema: { $ref: "#/components/schemas/Monitor" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteMonitor
      summary: Delete monitor
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /monitors/{id}/pause:
    post:
      operationId: pauseMonitor
      summary: Pause a monitor
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object, properties: { until: { type: string, format: date-time } } }
      responses:
        "200":
          {
            description: Paused,
            content: { application/json: { schema: { $ref: "#/components/schemas/Monitor" } } },
          }

  /monitors/{id}/resume:
    post:
      operationId: resumeMonitor
      summary: Resume a monitor
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          {
            description: Resumed,
            content: { application/json: { schema: { $ref: "#/components/schemas/Monitor" } } },
          }

  /monitors/{id}/uptime:
    get:
      operationId: getMonitorUptime
      summary: Uptime & latency summary
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: resolution, in: query, schema: { type: string, enum: [hour, day], default: day } }
      responses:
        "200":
          description: Uptime summary
          content: { application/json: { schema: { $ref: "#/components/schemas/UptimeSummary" } } }

  /monitors/{id}/checks:
    get:
      operationId: listMonitorChecks
      summary: Recent raw check results (<= 90 days)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 25 },
          }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Paginated raw checks
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/CheckResult" } }
                  next_cursor: { type: [string, "null"] }
                  has_more: { type: boolean }

  /monitors/{id}/incidents:
    get:
      operationId: listMonitorIncidents
      summary: Incidents scoped to one monitor
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 25 },
          }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Paginated incidents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Incident" } }
                  next_cursor: { type: [string, "null"] }
                  has_more: { type: boolean }

  /clients:
    get:
      operationId: listClients
      summary: List clients
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 25 },
          }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Paginated clients
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Client" } }
                  next_cursor: { type: [string, "null"] }
                  has_more: { type: boolean }
    post:
      operationId: createClient
      summary: Create a client
      parameters:
        - { name: Idempotency-Key, in: header, schema: { type: string, maxLength: 255 } }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/ClientCreate" } } }
      responses:
        "201":
          {
            description: Created,
            content: { application/json: { schema: { $ref: "#/components/schemas/Client" } } },
          }
        "403": { $ref: "#/components/responses/PlanLimit" }

  /clients/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      operationId: getClient
      summary: Fetch a client
      responses:
        "200":
          {
            description: Client,
            content: { application/json: { schema: { $ref: "#/components/schemas/Client" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateClient
      summary: Update a client
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content: { application/json: { schema: { $ref: "#/components/schemas/Client" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteClient
      summary: Delete a client
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /incidents:
    get:
      operationId: listIncidents
      summary: List incidents
      parameters:
        - {
            name: status,
            in: query,
            schema: { type: string, enum: [open, acknowledged, resolved] },
          }
        - { name: client_id, in: query, schema: { type: string } }
        - { name: monitor_id, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200":
          description: Paginated incidents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Incident" } }
                  next_cursor: { type: [string, "null"] }
                  has_more: { type: boolean }

  /incidents/{id}:
    get:
      operationId: getIncident
      summary: Fetch an incident with confirmation metadata
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          {
            description: Incident,
            content: { application/json: { schema: { $ref: "#/components/schemas/Incident" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /incidents/{id}/events:
    get:
      operationId: listIncidentEvents
      summary: Incident timeline
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Timeline
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/IncidentEvent" } }

  /incidents/{id}/acknowledge:
    post:
      operationId: acknowledgeIncident
      summary: Acknowledge an open incident
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          {
            description: Acknowledged,
            content: { application/json: { schema: { $ref: "#/components/schemas/Incident" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /incidents/{id}/resolve:
    post:
      operationId: resolveIncident
      summary: Manually resolve an incident
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          {
            description: Resolved,
            content: { application/json: { schema: { $ref: "#/components/schemas/Incident" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /status-pages:
    get:
      operationId: listStatusPages
      summary: List status pages
      responses:
        "200":
          description: Status pages
          content:
            application/json:
              schema:
                {
                  type: object,
                  properties:
                    { data: { type: array, items: { $ref: "#/components/schemas/StatusPage" } } },
                }
    post:
      operationId: createStatusPage
      summary: Create a status page
      requestBody:
        {
          required: true,
          content:
            {
              application/json:
                {
                  schema:
                    {
                      type: object,
                      required: [title],
                      properties:
                        {
                          client_id: { type: [string, "null"] },
                          title: { type: string },
                          slug: { type: string },
                        },
                    },
                },
            },
        }
      responses:
        "201":
          {
            description: Created,
            content: { application/json: { schema: { $ref: "#/components/schemas/StatusPage" } } },
          }
        "409": { description: Slug already taken }

  /status-pages/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      operationId: getStatusPage
      summary: Fetch config
      responses:
        "200":
          {
            description: Status page,
            content: { application/json: { schema: { $ref: "#/components/schemas/StatusPage" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateStatusPage
      summary: Update branding/monitors/noindex
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content: { application/json: { schema: { $ref: "#/components/schemas/StatusPage" } } },
          }
    delete:
      operationId: deleteStatusPage
      summary: Delete page
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }

  /alert-channels:
    get:
      operationId: listAlertChannels
      summary: List channels (secrets redacted)
      responses:
        "200":
          {
            description: Channels,
            content:
              {
                application/json:
                  {
                    schema:
                      {
                        type: object,
                        properties:
                          {
                            data:
                              { type: array, items: { $ref: "#/components/schemas/AlertChannel" } },
                          },
                      },
                  },
              },
          }
    post:
      operationId: createAlertChannel
      summary: Create channel
      requestBody:
        {
          required: true,
          content: { application/json: { schema: { type: object, required: [type, name] } } },
        }
      responses:
        "201":
          {
            description: Created,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/AlertChannel" } } },
          }
        "403": { description: Channel type not available on this plan }

  /alert-channels/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    patch:
      operationId: updateAlertChannel
      summary: Update config/events filter
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/AlertChannel" } } },
          }
    delete:
      operationId: deleteAlertChannel
      summary: Delete channel
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }

  /alert-channels/{id}/test:
    post:
      operationId: testAlertChannel
      summary: Send a test notification
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Test sent }
        "422": { description: Test delivery failed }

  /alert-channels/{id}/deliveries:
    get:
      operationId: listAlertChannelDeliveries
      summary: Delivery log, last 30 days
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { description: Deliveries }

  /alert-rules:
    get:
      operationId: listAlertRules
      summary: List routing rules
      parameters:
        - { name: monitor_id, in: query, schema: { type: string } }
        - { name: client_id, in: query, schema: { type: string } }
      responses:
        "200":
          {
            description: Rules,
            content:
              {
                application/json:
                  {
                    schema:
                      {
                        type: object,
                        properties:
                          {
                            data:
                              { type: array, items: { $ref: "#/components/schemas/AlertRule" } },
                          },
                      },
                  },
              },
          }
    post:
      operationId: createAlertRule
      summary: Create rule
      requestBody:
        {
          required: true,
          content: { application/json: { schema: { type: object, required: [channel_id] } } },
        }
      responses:
        "201":
          {
            description: Created,
            content: { application/json: { schema: { $ref: "#/components/schemas/AlertRule" } } },
          }

  /alert-rules/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    patch:
      operationId: updateAlertRule
      summary: Update rule
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content: { application/json: { schema: { $ref: "#/components/schemas/AlertRule" } } },
          }
    delete:
      operationId: deleteAlertRule
      summary: Delete rule
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }

  /maintenance-windows:
    get:
      operationId: listMaintenanceWindows
      summary: List windows
      parameters:
        - { name: monitor_id, in: query, schema: { type: string } }
        - { name: active, in: query, schema: { type: boolean } }
      responses:
        "200":
          {
            description: Windows,
            content:
              {
                application/json:
                  {
                    schema:
                      {
                        type: object,
                        properties:
                          {
                            data:
                              {
                                type: array,
                                items: { $ref: "#/components/schemas/MaintenanceWindow" },
                              },
                          },
                      },
                  },
              },
          }
    post:
      operationId: createMaintenanceWindow
      summary: Create one-off or RRULE-recurring window
      requestBody:
        {
          required: true,
          content:
            {
              application/json: { schema: { type: object, required: [name, starts_at, ends_at] } },
            },
        }
      responses:
        "201":
          {
            description: Created,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/MaintenanceWindow" } } },
          }

  /maintenance-windows/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      operationId: getMaintenanceWindow
      summary: Fetch a window
      responses:
        "200":
          {
            description: Window,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/MaintenanceWindow" } } },
          }
    patch:
      operationId: updateMaintenanceWindow
      summary: Update
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/MaintenanceWindow" } } },
          }
    delete:
      operationId: deleteMaintenanceWindow
      summary: Delete
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }

  /reports:
    get:
      operationId: listReports
      summary: List generated reports
      parameters:
        - { name: client_id, in: query, schema: { type: string } }
      responses:
        "200":
          {
            description: Reports,
            content:
              {
                application/json:
                  {
                    schema:
                      {
                        type: object,
                        properties:
                          { data: { type: array, items: { $ref: "#/components/schemas/Report" } } },
                      },
                  },
              },
          }
    post:
      operationId: createReport
      summary: Generate on-demand report (async)
      requestBody:
        {
          required: true,
          content:
            {
              application/json:
                { schema: { type: object, required: [client_id, period_start, period_end] } },
            },
        }
      responses:
        "201":
          {
            description: Queued,
            content: { application/json: { schema: { $ref: "#/components/schemas/Report" } } },
          }

  /reports/{id}:
    get:
      operationId: getReport
      summary: Status + short-lived signed download_url
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          {
            description: Report,
            content: { application/json: { schema: { $ref: "#/components/schemas/Report" } } },
          }
        "404": { $ref: "#/components/responses/NotFound" }

  /report-schedules:
    get:
      operationId: listReportSchedules
      summary: List schedules
      responses:
        "200":
          {
            description: Schedules,
            content:
              {
                application/json:
                  {
                    schema:
                      {
                        type: object,
                        properties:
                          {
                            data:
                              {
                                type: array,
                                items: { $ref: "#/components/schemas/ReportSchedule" },
                              },
                          },
                      },
                  },
              },
          }
    post:
      operationId: createReportSchedule
      summary: Create monthly schedule
      requestBody:
        {
          required: true,
          content:
            { application/json: { schema: { type: object, required: [client_id, recipients] } } },
        }
      responses:
        "201":
          {
            description: Created,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/ReportSchedule" } } },
          }

  /report-schedules/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    patch:
      operationId: updateReportSchedule
      summary: Update
      requestBody: { content: { application/json: { schema: { type: object } } } }
      responses:
        "200":
          {
            description: Updated,
            content:
              { application/json: { schema: { $ref: "#/components/schemas/ReportSchedule" } } },
          }
    delete:
      operationId: deleteReportSchedule
      summary: Delete
      responses:
        "200":
          {
            description: Deleted,
            content: { application/json: { schema: { $ref: "#/components/schemas/Ok" } } },
          }

  /heartbeat/{token}:
    post:
      operationId: heartbeatPing
      summary: Cron heartbeat ping (capability URL, no bearer auth)
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                status: { type: string, enum: [ok] }
                message: { type: string, maxLength: 500 }
                duration_ms: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Timer reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  received_at: { type: string, format: date-time }
                  next_expected_by: { type: string, format: date-time }
        "404": { description: Unknown token (empty body) }
        "429": { description: Rate-limited (timer still reset) }
    get:
      operationId: heartbeatPingGet
      summary: Same as POST, GET form (for bare `curl` cron one-liners)
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Timer reset }
        "404": { description: Unknown token }

  /heartbeat/{token}/fail:
    post:
      operationId: heartbeatFail
      summary: Explicit failure report — opens an incident immediately
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object, properties: { message: { type: string, maxLength: 500 } } }
      responses:
        "200": { description: Incident opened (or already active) }
        "404": { description: Unknown token }
    get:
      operationId: heartbeatFailGet
      summary: Same as POST, GET form
      security: []
      parameters:
        - { name: token, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Incident opened (or already active) }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: "pd_{keyId}_{secret}" }
  responses:
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PlanLimit:
      description: Plan quota reached (soft block)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No such resource in this agency
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unprocessable:
      description: Semantically invalid (e.g. SSRF-blocked target)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Ok:
      type: object
      properties: { ok: { type: boolean } }
    MonitorType: { type: string, enum: [http, ssl, domain, tcp, ping, heartbeat] }
    Monitor:
      type: object
      required: [id, client_id, name, type, interval_seconds, status, paused, created_at]
      properties:
        id: { type: string }
        client_id: { type: [string, "null"] }
        name: { type: string, maxLength: 120 }
        type: { $ref: "#/components/schemas/MonitorType" }
        url: { type: [string, "null"], format: uri }
        target: { type: [string, "null"] }
        port: { type: [integer, "null"] }
        interval_seconds: { type: integer, minimum: 60 }
        timeout_ms: { type: integer }
        confirmations: { type: integer }
        require_secondary: { type: boolean }
        config: { type: object }
        status: { type: string, enum: [pending, up, down, paused] }
        paused: { type: boolean }
        paused_reason: { type: [string, "null"] }
        last_checked_at: { type: [string, "null"], format: date-time }
        last_response_ms: { type: [integer, "null"] }
        ssl_expires_at: { type: [string, "null"], format: date-time }
        domain_expires_at: { type: [string, "null"], format: date-time }
        heartbeat_url: { type: [string, "null"] }
        heartbeat_configured: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    MonitorCreate:
      type: object
      required: [type]
      properties:
        client_id: { type: [string, "null"] }
        name: { type: string }
        type: { $ref: "#/components/schemas/MonitorType" }
        url: { type: string, format: uri }
        host: { type: string }
        port: { type: integer }
        interval_seconds: { type: integer }
        timeout_ms: { type: integer }
        confirmation_retries: { type: integer, default: 2 }
        regions_required: { type: integer, default: 2 }
        http: { type: object }
        tcp: { type: object }
        heartbeat: { type: object }
        config: { type: object }
    UptimeSummary:
      type: object
      properties:
        monitor_id: { type: string }
        from: { type: string, format: date }
        to: { type: string, format: date }
        resolution: { type: string, enum: [hour, day] }
        uptime_pct: { type: [number, "null"] }
        downtime_seconds: { type: integer }
        incident_count: { type: integer }
        latency_ms:
          type: object
          properties: { p50: { type: [integer, "null"] }, p95: { type: [integer, "null"] } }
        maintenance_excluded: { type: boolean }
        buckets: { type: array, items: { type: object } }
        source: { type: string, const: rollups_hourly }
    CheckResult:
      type: object
      properties:
        checked_at: { type: string, format: date-time }
        probe: { type: string }
        ok: { type: boolean }
        status_code: { type: [integer, "null"] }
        response_ms: { type: [integer, "null"] }
        error_code: { type: [string, "null"] }
    Client:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        contact_email: { type: [string, "null"] }
        logo_url: { type: [string, "null"] }
        notes: { type: string }
        monitors:
          type: object
          properties: { up: { type: integer }, down: { type: integer }, total: { type: integer } }
        archived_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ClientCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        contact_email: { type: [string, "null"] }
        logo_url: { type: [string, "null"] }
        notes: { type: string }
    Incident:
      type: object
      properties:
        id: { type: string }
        monitor_id: { type: string }
        client_id: { type: [string, "null"] }
        status: { type: string, enum: [open, acknowledged, resolved] }
        cause: { type: string }
        detail: { type: [string, "null"] }
        is_maintenance: { type: boolean }
        alerted: { type: boolean }
        first_failed_at: { type: string, format: date-time }
        confirmed_primary_at: { type: [string, "null"], format: date-time }
        confirmed_secondary_at: { type: [string, "null"], format: date-time }
        opened_at: { type: [string, "null"], format: date-time }
        acknowledged_at: { type: [string, "null"], format: date-time }
        acknowledged_by: { type: [string, "null"] }
        resolved_at: { type: [string, "null"], format: date-time }
        duration_seconds: { type: [integer, "null"] }
        confirmations:
          type: array
          items:
            type: object
            properties:
              {
                probe: { type: string },
                failed_at: { type: string, format: date-time },
                attempts: { type: integer },
              }
    IncidentEvent:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        at: { type: string, format: date-time }
        payload: { type: object }
    StatusPage:
      type: object
      properties:
        id: { type: string }
        client_id: { type: [string, "null"] }
        slug: { type: string }
        title: { type: string }
        logo_url: { type: [string, "null"] }
        theme: { type: object }
        noindex: { type: boolean }
        is_published: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AlertChannel:
      type: object
      properties:
        id: { type: string }
        type: { type: string, enum: [email, telegram, slack, discord, webhook] }
        name: { type: string }
        config: { type: object }
        secret_configured: { type: boolean }
        is_enabled: { type: boolean }
        last_used_at: { type: [string, "null"], format: date-time }
        last_error: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AlertRule:
      type: object
      properties:
        id: { type: string }
        channel_id: { type: string }
        monitor_id: { type: [string, "null"] }
        client_id: { type: [string, "null"] }
        notify_open: { type: boolean }
        notify_resolve: { type: boolean }
        notify_ssl_expiry: { type: boolean }
        notify_domain_expiry: { type: boolean }
        min_downtime_seconds: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    MaintenanceWindow:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        monitor_id: { type: [string, "null"] }
        client_id: { type: [string, "null"] }
        starts_at: { type: string, format: date-time }
        ends_at: { type: string, format: date-time }
        rrule: { type: [string, "null"] }
        suppress_alerts: { type: boolean }
        exclude_from_uptime: { type: boolean }
        active: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Report:
      type: object
      properties:
        id: { type: string }
        client_id: { type: string }
        schedule_id: { type: [string, "null"] }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }
        status: { type: string, enum: [queued, rendering, rendered, sending, sent, failed, purged] }
        size_bytes: { type: [integer, "null"] }
        sent_to: { type: [array, "null"], items: { type: string } }
        error: { type: [string, "null"] }
        generated_at: { type: [string, "null"], format: date-time }
        sent_at: { type: [string, "null"], format: date-time }
        download_url: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    ReportSchedule:
      type: object
      properties:
        id: { type: string }
        client_id: { type: string }
        smtp_connection_id: { type: [string, "null"] }
        cadence: { type: string }
        send_day: { type: integer }
        send_hour: { type: integer }
        timezone: { type: string }
        recipients: { type: array, items: { type: string } }
        bcc_agency: { type: boolean }
        subject_template: { type: string }
        enabled: { type: boolean }
        last_run_at: { type: [string, "null"], format: date-time }
        next_run_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Me:
      type: object
      properties:
        agency_id: { type: string }
        agency_name: { type: [string, "null"] }
        plan: { type: string, enum: [free, starter, pro, agency] }
        scopes: { type: array, items: { type: string } }
        rate_limits:
          type: object
          properties:
            {
              sustained_per_min: { type: integer },
              burst_per_10s: { type: integer },
              write_per_min: { type: integer },
            }
        quota: { type: object }
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: array, items: { type: object } }
            request_id: { type: string }
            doc_url: { type: string }
