openapi: 3.1.0
info:
  title: Watchfox API
  version: "v1 (updated 2026-06-14)"
  x-docs-last-updated: "2026-06-14"
  license:
    name: Proprietary
  description: |
    Watchfox HTTP API reference, separated by integration surface.

    Integration surfaces:
    - Public API: customer-facing, project-scoped endpoints authenticated with `X-Api-Key`.
    - CSV exports: project-scoped export endpoints for BI/reporting workflows.
    - Report exports: project-scoped PDF/summary report endpoints, plan-gated where applicable.
    - Utility validators: unauthenticated, per-IP rate-limited diagnostic tools.
    - UI-managed/admin endpoints: signed-in app flows authenticated with Bearer JWT.

    Authentication:
    - API key: send `X-Api-Key: wk_...`.
    - Bearer JWT: send `Authorization: Bearer <supabase_access_token>` and pass `project_id` or `org_id` where required.
    - Utility validators do not require auth, but are rate-limited per IP.

    Common headers:
    - JSON/CSV endpoints may include `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`.
    - 429 responses may include `Retry-After`.
    - CORS preflights return HTTP 204.

servers:
  - url: https://app.watchfox.io
    description: Production
  - url: https://develop.watchfox-app.pages.dev
    description: Staging

tags:
  - name: Public API
    description: Stable customer integration surface authenticated with project API keys.
  - name: CSV exports
    description: Project-scoped CSV export endpoints for reporting, BI, and support workflows.
  - name: Report exports
    description: Project-scoped PDF and report-summary endpoints; plan-gated where applicable.
  - name: Utilities
    description: Unauthenticated public utility validators, redirect helpers, and heartbeat ping endpoints.
  - name: UI-managed/admin
    description: Signed-in app/admin endpoints authenticated with Bearer JWT; not the main Public API integration contract.

# Default: no auth required unless overridden per-operation.
# This also satisfies Redocly "security-defined" rule for OPTIONS/health endpoints.
security: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    ErrorResponse:
      type: object
      additionalProperties: true
      properties:
        error:
          type: string
        detail:
          type: [string, "null"]
        code:
          type: [string, "null"]
        limit:
          type: [integer, "null"]
        current:
          type: [integer, "null"]
        missing:
          type: [array, "null"]
          items:
            type: string

    Monitor:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        type:
          type: string
          enum: [uptime, tls, heartbeat, sitemap, keyword, domain]
        label:
          type: string
        url:
          type: [string, "null"]
          format: uri
        enabled:
          type: boolean
        interval_s:
          type: integer
          description: Check cadence in seconds. For tls monitors this is always 43200 (12 hours). For domain and sitemap monitors this is always 86400 (24 hours).
        timeout_ms:
          type: [integer, "null"]
        locations:
          type: array
          description: Backward-compatible/internal probe group codes. Normal scheduled Uptime/Keyword checks use one primary scheduled probe group; additional probe coverage is automatic for smart outage verification where supported. Do not use this field to request permanent multi-probe scheduled checking.
          items:
            type: string
        expected_status:
          type: [integer, "null"]
        keyword:
          type: [string, "null"]
        keyword_should_exist:
          type: [boolean, "null"]
        heartbeat_url:
          type: [string, "null"]
          format: uri
        deleted_at:
          type: [string, "null"]
          format: date-time

    CreateMonitorRequest:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          type: string
          enum: [uptime, tls, heartbeat, sitemap, keyword, domain]
        url:
          type: [string, "null"]
          format: uri
        label:
          type: string
        enabled:
          type: boolean
        interval_s:
          type: integer
          description: Optional for interval-driven monitors. Ignored for tls monitors (always 43200 / 12 hours) and for domain and sitemap monitors (always 86400 / 24 hours).
        timeout_ms:
          type: integer
        locations:
          type: array
          description: Backward-compatible/internal compatibility field. Accepted for compatibility and normalized to one primary scheduled probe group for normal scheduled Uptime/Keyword checks; not a supported customer configuration knob for permanent multi-probe scheduled checking.
          items:
            type: string
        expected_status:
          type: integer
        keyword:
          type: string
        keyword_should_exist:
          type: boolean

    UpdateMonitorRequest:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [uptime, tls, heartbeat, sitemap, keyword, domain]
        url:
          type: [string, "null"]
          format: uri
        label:
          type: string
        enabled:
          type: boolean
        interval_s:
          type: integer
          description: Optional for interval-driven monitors. Ignored for tls monitors, which always use 43200 (12 hours).
        timeout_ms:
          type: [integer, "null"]
        locations:
          type: [array, "null"]
          description: Backward-compatible/internal compatibility field. Accepted for compatibility and normalized to one primary scheduled probe group for normal scheduled Uptime/Keyword checks; not a supported customer configuration knob for permanent multi-probe scheduled checking.
          items:
            type: string
        expected_status:
          type: [integer, "null"]
        keyword:
          type: [string, "null"]
        keyword_should_exist:
          type: [boolean, "null"]

    MonitorCheck:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
          format: uuid
        monitor_id:
          type: string
          format: uuid
        checked_at:
          type: string
          format: date-time
        region:
          type: string
          description: Internal probe group code. This is not a guaranteed geographic execution location.
        status_code:
          type: [integer, "null"]
        latency_ms:
          type: [integer, "null"]
        ok:
          type: boolean
        error:
          type: [string, "null"]
        muted_by_mw:
          type: [boolean, "null"]

    Incident:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
          format: uuid
        monitor_id:
          type: string
          format: uuid
        started_at:
          type: string
          format: date-time
        resolved_at:
          type: [string, "null"]
          format: date-time
        summary:
          type: [string, "null"]
        status:
          type: [string, "null"]
        details:
          type: [object, "null"]
          additionalProperties: true
        monitor_label:
          type: [string, "null"]
        monitor_url:
          type: [string, "null"]
          format: uri

    TestAlertRequest:
      type: object
      additionalProperties: true
      required: [monitor_id]
      properties:
        monitor_id:
          type: string
          format: uuid
        project_id:
          type: [string, "null"]
          format: uuid
          description: Required when using BearerAuth.

    ReportSummary:
      type: object
      additionalProperties: true

    ApiKeySummary:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        last8:
          type: string
        created_at:
          type: string
          format: date-time
        revoked_at:
          type: [string, "null"]
          format: date-time
        key:
          type: [string, "null"]
          description: Returned only at creation time.

    NotificationChannel:
      type: object
      additionalProperties: true
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        channel:
          type: string
          enum: [email, slack, teams, discord, google_chat, webhook]
        target:
          type: string
        rules:
          type: [object, "null"]
          additionalProperties: true

    NotificationEvent:
      type: object
      additionalProperties: true

paths:
  /api/v1/monitors:
    options:
      operationId: corsPreflightMonitors
      tags: [Public API]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: listMonitors
      tags: [Public API]
      summary: List monitors
      description: |
        Lists monitors for the scoped project.
        - ApiKeyAuth scopes to the key's project.
        - BearerAuth supported for UI; pass `project_id` to select a project.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: include_deleted
          schema: { type: boolean }
          description: If true, include soft-deleted monitors.
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: When using BearerAuth, selects a project. Ignored for ApiKeyAuth.
      responses:
        "200":
          description: Array of monitors
          headers:
            RateLimit-Limit: { schema: { type: string } }
            RateLimit-Remaining: { schema: { type: string } }
            RateLimit-Reset: { schema: { type: string } }
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Monitor" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Forbidden
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

    post:
      operationId: createMonitor
      tags: [Public API]
      summary: Create a monitor
      description: Creates a monitor. Plan/limit enforcement may return HTTP 402. TLS monitors use a fixed 12-hour cadence and ignore custom interval_s. Domain and sitemap monitors use a fixed 24-hour cadence and also ignore custom interval_s.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth (UI). Ignored for ApiKeyAuth.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateMonitorRequest" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Monitor" }
        "400":
          description: Invalid input
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "402":
          description: Plan limit exceeded
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limit exceeded

    patch:
      operationId: updateMonitor
      tags: [Public API]
      summary: Update a monitor
      description: |
        Partial update. `id` is required in the JSON body.
        TLS monitors use a fixed 12-hour cadence and ignore custom interval_s.
        Domain and sitemap monitors use a fixed 24-hour cadence and also ignore custom interval_s.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth (UI). Ignored for ApiKeyAuth.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateMonitorRequest" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Monitor" }
        "400":
          description: Invalid input
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "402":
          description: Plan limit exceeded
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

    delete:
      operationId: deleteMonitor
      tags: [Public API]
      summary: Soft-delete a monitor
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string, format: uuid }
          description: Monitor id to delete (soft delete).
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth (UI). Ignored for ApiKeyAuth.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { description: Invalid input }
        "401": { description: Unauthorized }

  /api/v1/monitors/bulk:
    options:
      operationId: corsPreflightMonitorsBulk
      tags: [Public API]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    post:
      operationId: bulkUpsertMonitors
      tags: [Public API]
      summary: Bulk create or update monitors
      description: |
        Batch create monitors for imports/migrations. Applies the same server-side validation and plan-limit enforcement as single monitor creation.

        Probe coverage is automatic for normal scheduled Uptime and Keyword checks.
        `probe_groups`, `probe_group`, and legacy `locations` are accepted only as backward-compatible/internal compatibility fields.
        Multiple submitted probe-group values are normalized to one primary scheduled probe group and do not enable permanent multi-probe scheduled checking.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth (UI). Ignored for ApiKeyAuth.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                dry_run:
                  type: boolean
                  default: true
                items:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    additionalProperties: true
                    properties:
                      type:
                        type: string
                        enum: [uptime, tls, heartbeat, sitemap, keyword, domain]
                      url:
                        type: [string, "null"]
                      label:
                        type: string
                      interval_s:
                        type: integer
                      timeout_ms:
                        type: integer
                      enabled:
                        type: boolean
                      probe_groups:
                        type: [string, array]
                        description: Backward-compatible compatibility input. Accepted but normalized to one primary scheduled probe group; not recommended for new customer imports.
                        items: { type: string }
                      probe_group:
                        type: string
                        description: Singular backward-compatible alias for `probe_groups`; also normalized to one primary scheduled probe group.
                      locations:
                        type: [string, array]
                        description: Legacy/internal compatibility alias. Accepted but normalized to one primary scheduled probe group.
                        items: { type: string }
                      expected_status:
                        type: integer
                      keyword:
                        type: string
                      keyword_should_exist:
                        type: boolean
              required: [items]
            examples:
              preview:
                summary: Preview one uptime monitor
                value:
                  dry_run: true
                  items:
                    - type: uptime
                      url: https://example.com
                      label: Homepage
                      interval_s: 300
      responses:
        "200": { description: OK }
        "201": { description: Created }
        "400": { description: Invalid input }
        "401": { description: Unauthorized }
        "402": { description: Plan limit exceeded }
        "429": { description: Rate limit exceeded }

  /api/v1/monitor_checks:
    options:
      operationId: corsPreflightMonitorChecks
      tags: [Public API]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: listMonitorChecks
      tags: [Public API]
      summary: List monitor checks
      description: |
        Lists recent raw checks. Supports cursor paging (`before`) or offset paging (`offset`).
        Raw `monitor_checks` history is available only for the last 30 days.
        Older history for reporting is served from aggregated sources, not from this endpoint.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: When using BearerAuth, selects a project.
        - in: query
          name: monitor_id
          schema: { type: string, format: uuid }
        - in: query
          name: type
          schema: { type: string, enum: [uptime, tls, heartbeat, sitemap, keyword, domain] }
        - in: query
          name: region
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 2000, default: 200 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0 }
        - in: query
          name: before
          schema: { type: string }
          description: "Cursor for descending paging: <checked_at> or <checked_at>,<id>."
      responses:
        "200":
          description: Array of checks
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/MonitorCheck" }
        "400": { description: Invalid input }
        "401": { description: Unauthorized }
        "403": { description: Forbidden }

  /api/v1/incidents:
    options:
      operationId: corsPreflightIncidents
      tags: [Public API]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: listIncidents
      tags: [Public API]
      summary: List incidents
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date-time }
        - in: query
          name: to
          schema: { type: string, format: date-time }
        - in: query
          name: monitor_id
          schema: { type: string, format: uuid }
        - in: query
          name: status
          schema: { type: string, enum: [open, resolved, all], default: all }
        - in: query
          name: include
          schema: { type: string, enum: [monitor, monitor_label, monitor_url] }
        - in: query
          name: since
          schema: { type: string }
          description: Cursor (ISO or `started_at,id`). Forces ascending order.
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc] }
          description: When `since` is used, order is forced to `asc`.
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 50000, default: 20000 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Array of incidents
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Incident" }
        "401": { description: Missing/invalid API key }
        "429": { description: Rate limit exceeded }

  /api/v1/test-alert:
    options:
      operationId: corsPreflightTestAlert
      tags: [Public API]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    post:
      operationId: sendTestAlert
      tags: [Public API]
      summary: Send a test alert (no incident)
      description: |
        Sends a TEST notification through configured channels.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TestAlertRequest" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { description: Bad request }
        "401": { description: Unauthorized }
        "403": { description: Forbidden }
        "404": { description: Not found }

  /api/v1/status/{slug}:
    parameters:
      - in: path
        name: slug
        required: true
        schema: { type: string }

    options:
      operationId: corsPreflightStatusRedirect
      tags: [Utilities]
      summary: CORS preflight
      security: []
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    head:
      operationId: statusRedirectHead
      tags: [Utilities]
      summary: Redirect helper (HEAD)
      security: []
      responses:
        "200": { description: OK (non-redirect fallback) }
        "308": { description: "Permanent redirect to /status/{slug}" }
        "404": { description: Not found }

    get:
      operationId: statusRedirectGet
      tags: [Utilities]
      summary: Redirect helper (GET)
      security: []
      responses:
        "200": { description: OK (non-redirect fallback) }
        "308": { description: "Permanent redirect to /status/{slug}" }
        "404": { description: Not found }

  /api/export/monitor_checks.csv:
    options:
      operationId: corsPreflightExportMonitorChecks
      tags: [CSV exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: exportMonitorChecksCsv
      tags: [CSV exports]
      summary: Export monitor checks as CSV
      description: |
        Export recent raw `monitor_checks` rows as CSV.

        Default customer-facing columns:
        `monitor_id,checked_at,probe_group,status_code,latency_ms,ok`

        `probe_group` identifies the neutral probe group that produced the historical check evidence. It is not a customer scheduling control.

        Raw history is available only for the last 30 days. Requests older than
        the raw retention window return `raw_retention_window_exceeded`.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date-time }
        - in: query
          name: to
          schema: { type: string, format: date-time }
        - in: query
          name: monitor_id
          required: true
          schema: { type: string, format: uuid }
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth. Ignored for ApiKeyAuth.
        - in: query
          name: ok
          schema: { type: boolean }
        - in: query
          name: since
          schema: { type: string }
          description: Cursor (`<ISO>` or `checked_at,id`). Forces ascending order.
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 2000, default: 1000 }
        - in: query
          name: fields
          schema: { type: string }
          description: |
            Optional comma-separated CSV fields.

            Allowed:
            `monitor_id,checked_at,probe_group,status_code,latency_ms,ok,id,muted_by_mw`
          example: monitor_id,checked_at,probe_group,status_code,latency_ms,ok
      responses:
        "200":
          description: CSV
          headers:
            RateLimit-Limit: { schema: { type: string } }
            RateLimit-Remaining: { schema: { type: string } }
            RateLimit-Reset: { schema: { type: string } }
            X-Watchfox-Next-Since:
              schema: { type: string }
              description: Cursor for the next page in the form `checked_at,id`.
          content:
            text/csv:
              schema: { type: string }
              examples:
                default:
                  summary: Default customer-facing CSV
                  value: |
                    monitor_id,checked_at,probe_group,status_code,latency_ms,ok
                    00000000-0000-0000-0000-000000000000,2026-04-03T17:00:14Z,Probe group 1,200,193,true
        "400": { description: Bad request or raw retention window exceeded }
        "401": { description: Missing/invalid API key or Bearer session }
        "429": { description: Rate limit exceeded }

  /api/export/incidents.csv:
    options:
      operationId: corsPreflightExportIncidents
      tags: [CSV exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: exportIncidentsCsv
      tags: [CSV exports]
      summary: Export incidents as CSV
      description: |
        Project-scoped incident export.

        Default behavior keeps backward-compatible started-at filtering when `from`/`to` are supplied.

        For report-linked exports, pass `scope=report`. In report scope the window uses `[from, to)` semantics and incident rows are included when they overlap the report window:
        `started_at < to AND (resolved_at IS NULL OR resolved_at > from)`.

        Report scope also adds clipping columns so long-running incidents clearly show the portion that affected the selected report period.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth. Ignored for ApiKeyAuth.
        - in: query
          name: from
          schema: { type: string, format: date-time }
          description: Window start. Required for `scope=report`.
        - in: query
          name: to
          schema: { type: string, format: date-time }
          description: Window end. In `scope=report`, this is exclusive.
        - in: query
          name: scope
          schema: { type: string, enum: [report] }
          description: Use `report` for overlap semantics and clipping columns.
        - in: query
          name: range_mode
          schema: { type: string, enum: [overlap] }
          description: Compatibility alias for `scope=report` overlap semantics.
        - in: query
          name: monitor_id
          schema: { type: string, format: uuid }
        - in: query
          name: status
          schema: { type: string, enum: [open, resolved, all], default: all }
        - in: query
          name: since
          schema: { type: string }
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 2000, default: 1000 }
        - in: query
          name: include
          schema: { type: string, enum: [monitor, monitor_label, monitor_url] }
      responses:
        "200":
          description: CSV
          content:
            text/csv:
              schema: { type: string }
              examples:
                reportScope:
                  summary: Report-linked export with overlap/clipping columns
                  value: |
                    incident_id,monitor_id,started_at,resolved_at,summary,status,clip_started_at,clip_ended_at,duration_in_period_sec,started_before_period,continues_beyond_period,monitor_label
                    00000000-0000-0000-0000-000000000000,11111111-1111-1111-1111-111111111111,2026-05-28T23:00:00Z,2026-05-30T01:00:00Z,DOWN: example.com,resolved,2026-05-29T00:00:00.000Z,2026-05-30T00:00:00.000Z,86400,true,true,Website uptime
        "400": { description: Bad request or invalid report window }
        "401": { description: Missing/invalid API key or Bearer session }
        "429": { description: Rate limit exceeded }

  /api/export/report_summary.csv:
    options:
      operationId: corsPreflightExportReportSummary
      tags: [CSV exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: exportReportSummaryCsv
      tags: [CSV exports]
      summary: Export report summary as CSV
      description: |
        Exports report-window summary rows and included web-monitor metrics.

        Auth:
        - ApiKeyAuth scopes to the key's project.
        - BearerAuth requires `project_id`.

        Intended for reporting, BI, and customer portal export workflows.
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: month
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
          description: Required when using BearerAuth. Ignored for ApiKeyAuth.
      responses:
        "200":
          description: CSV with report summary rows
          content:
            text/csv:
              schema: { type: string }
        "400": { description: Invalid range/month }
        "401": { description: Missing/invalid API key or auth token }
        "429": { description: Rate limit exceeded }

  /api/export/link_errors.csv:
    options:
      operationId: corsPreflightExportLinkErrors
      tags: [CSV exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: exportLinkErrorsCsv
      tags: [Exports]
      summary: Export problematic links as CSV
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: url
          required: true
          schema: { type: string, format: uri }
        - in: query
          name: sample
          schema: { type: integer, minimum: 1, maximum: 200, default: 200 }
        - in: query
          name: timeout_ms
          schema: { type: integer, minimum: 1000, maximum: 30000, default: 8000 }
        - in: query
          name: concurrency
          schema: { type: integer, minimum: 1, maximum: 25, default: 10 }
      responses:
        "200":
          description: CSV with columns `source_url,target_url,status,ms,error`
          content:
            text/csv:
              schema: { type: string }
        "401": { description: Missing/invalid API key }
        "429": { description: Rate limit exceeded }

  /api/reports/monthly.pdf:
    options:
      operationId: corsPreflightReportsMonthlyPdf
      tags: [Report exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: getMonthlyReportPdf
      tags: [Exports]
      summary: Download monthly PDF report
      description: |
        Downloads a monthly or custom-range PDF report.

        Auth:
        - ApiKeyAuth scopes to the key's project.
        - BearerAuth requires `project_id`.

        This is a report export endpoint, not a high-frequency polling API.
        It is plan-gated by the `pdf_reports` entitlement.

        Historical read model:
        - 0–30 days: raw `monitor_checks`
        - 31–90 days: `monitor_check_rollups_15m`
        - 91 days+: `monitor_uptime_daily` + `incidents`    
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: month
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: PDF report
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401": { description: Missing/invalid API key or auth token }
        "402": { description: Plan required / gated }
        "429": { description: Rate limit exceeded }

  /api/reports/summary.json:
    options:
      operationId: corsPreflightReportsSummaryJson
      tags: [Report exports]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: getReportSummaryJson
      tags: [Exports]
      summary: Get report summary JSON
      description: |
        Returns the report summary used by dashboard/report workflows.

        Auth:
        - ApiKeyAuth scopes to the key's project.
        - BearerAuth requires `project_id`.

        This is a report export/summary endpoint. Cache where practical and avoid high-frequency polling.

        Historical read model:
        - 0–30 days: raw `monitor_checks`
        - 31–90 days: `monitor_check_rollups_15m`
        - 91 days+: `monitor_uptime_daily` + `incidents`    
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - in: query
          name: month
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
        - in: query
          name: project_id
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Report summary
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReportSummary" }
        "401": { description: Missing/invalid API key or auth token }
        "402": { description: Plan required / gated }
        "429": { description: Rate limit exceeded }

  /api/heartbeat/{token}:
    parameters:
      - in: path
        name: token
        required: true
        schema: { type: string }

    get:
      operationId: heartbeatPing
      tags: [Utilities]
      summary: Heartbeat ping (unauthenticated)
      security: []
      responses:
        "204": { description: Recorded }
        "404": { description: Unknown or disabled monitor }

  /api/validate/sitemap:
    options:
      operationId: corsPreflightValidateSitemap
      tags: [Utilities]
      summary: CORS preflight
      security: []
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    head:
      operationId: validateSitemapHealth
      tags: [Utilities]
      summary: Health check
      security: []
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: validateSitemap
      tags: [Utilities]
      summary: Light sitemap validator
      description: |
        Public unauthenticated utility validator.
        Rate-limited per IP.
        Intended for one-off diagnostics and setup validation, not bulk crawling.      
      security: []
      parameters:
        - in: query
          name: url
          required: true
          schema: { type: string, format: uri }
        - in: query
          name: sample
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: timeout_ms
          schema: { type: integer, minimum: 1000, maximum: 20000, default: 8000 }
      responses:
        "200": { description: OK }
        "400": { description: Invalid input }
        "429": { description: Per-IP rate limit exceeded }

  /api/validate/links:
    options:
      operationId: corsPreflightValidateLinks
      tags: [Utilities]
      summary: CORS preflight
      security: []
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    head:
      operationId: validateLinksHealth
      tags: [Utilities]
      summary: Health check
      security: []
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: validateLinks
      tags: [Utilities]
      summary: Links validator
      security: []
      parameters:
        - in: query
          name: url
          required: true
          schema: { type: string, format: uri }
        - in: query
          name: sample
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: timeout_ms
          schema: { type: integer, minimum: 1000, maximum: 30000, default: 8000 }
      responses:
        "200": { description: OK }
        "400": { description: Invalid input }
        "429": { description: Per-IP rate limit exceeded }

  /api/keys:
    options:
      operationId: corsPreflightKeys
      tags: [UI-managed/admin]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: listApiKeys
      tags: [UI]
      summary: List API keys for a project
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Array of keys
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApiKeySummary" }
        "401": { description: Unauthorized }

    post:
      operationId: createApiKey
      tags: [UI]
      summary: Create an API key for a project
      description: Returns plaintext key once. Store it safely.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, name]
              properties:
                project_id: { type: string, format: uuid }
                name: { type: string, minLength: 2 }
      responses:
        "201":
          description: Key created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiKeySummary" }
        "401": { description: Unauthorized }
        "402": { description: Plan required/limit reached }

    delete:
      operationId: revokeApiKey
      tags: [UI]
      summary: Revoke an API key
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: OK }
        "401": { description: Unauthorized }

  /api/notifications:
    options:
      operationId: corsPreflightNotifications
      tags: [UI-managed/admin]
      summary: CORS preflight
      responses:
        "204": { description: No content }
        "400": { description: Bad request }

    get:
      operationId: listNotificationChannels
      tags: [UI]
      summary: List notification channels
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: project_id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Array of channels
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/NotificationChannel" }
        "401": { description: Unauthorized }

    post:
      operationId: createNotificationChannel
      tags: [UI]
      summary: Create a notification channel
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, channel, target]
              properties:
                project_id: { type: string, format: uuid }
                channel: { type: string, enum: [email, slack, teams, discord, google_chat, webhook] }
                target: { type: string }
                rules:
                  type: [object, "null"]
                  additionalProperties: true
      responses:
        "201": { description: Created }
        "401": { description: Unauthorized }
        "402": { description: Plan required/limit reached }

    delete:
      operationId: deleteNotificationChannel
      tags: [UI]
      summary: Delete a notification channel
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: OK }
        "401": { description: Unauthorized }