openapi: 3.0.3
info:
  title: Vartio API
  description: |
    REST API for Vartio, an EU-first website monitoring SaaS.
    Provides uptime monitoring (HTTP, SSL, DNS, Ping), alerting via multiple
    channels, incident tracking, and public status pages.
  version: 0.1.0
  contact:
    name: KalleWorks
    url: https://kalle.works

servers:
  - url: https://api.vartio.dev
    description: Production
  - url: http://localhost:8080
    description: Local development

security:
  - bearerAuth: []

tags:
  - name: Health
    description: Health and metrics endpoints
  - name: Auth
    description: Authentication and user management
  - name: Monitors
    description: Monitor CRUD and check data
  - name: Alert Channels
    description: Alert channel configuration
  - name: Status Pages
    description: Status page management
  - name: Public
    description: Public-facing endpoints (no auth required)

paths:
  /health:
    get:
      tags: [Health]
      summary: Health check
      security: []
      operationId: getHealth
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /metrics:
    get:
      tags: [Health]
      summary: Prometheus metrics
      security: []
      operationId: getMetrics
      responses:
        "200":
          description: Prometheus-formatted metrics
          content:
            text/plain:
              schema:
                type: string

  /public/status/{slug}:
    get:
      tags: [Public]
      summary: Get public status page
      security: []
      operationId: getPublicStatusPage
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          description: Status page slug
      responses:
        "200":
          description: Public status page with monitor statuses
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicStatusPageResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/register:
    post:
      tags: [Auth]
      summary: Register a new user and organization
      description: |
        Creates a new user account, a default organization, and returns a JWT token.
        Rate limited to 5 requests per minute per IP.
      security: []
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "201":
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/login:
    post:
      tags: [Auth]
      summary: Login with email and password
      description: |
        Authenticates user credentials and returns a JWT token.
        Rate limited to 10 requests per minute per IP.
      security: []
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Login successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Invalid email or password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/forgot-password:
    post:
      tags: [Auth]
      summary: Request password reset email
      description: |
        Sends a password reset email if the account exists. Always returns
        success to prevent email enumeration. Rate limited to 3 requests
        per minute per IP.
      security: []
      operationId: forgotPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForgotPasswordRequest"
      responses:
        "200":
          description: Reset email sent (if account exists)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password with token
      description: |
        Resets the user's password using a token from the reset email.
        Rate limited to 5 requests per minute per IP.
      security: []
      operationId: resetPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResetPasswordRequest"
      responses:
        "200":
          description: Password reset successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/me:
    get:
      tags: [Auth]
      summary: Get current user profile
      operationId: getMe
      responses:
        "200":
          description: Current user and organizations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/profile:
    put:
      tags: [Auth]
      summary: Update user profile
      operationId: updateProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Email already in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/auth/password:
    put:
      tags: [Auth]
      summary: Change password
      operationId: changePassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [current_password, new_password]
              properties:
                current_password:
                  type: string
                new_password:
                  type: string
                  minLength: 8
      responses:
        "200":
          description: Password changed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/monitors:
    get:
      tags: [Monitors]
      summary: List monitors
      description: |
        Lists all monitors for the current organization, enriched with
        current status, last response time, and 30-day uptime percentage.

        Optional `label` query parameter filters by Prometheus-style
        labels using PostgreSQL `@>` containment. Repeat the parameter
        for AND semantics: `?label=env:prod&label=team:platform` returns
        monitors with BOTH labels.
      operationId: listMonitors
      parameters:
        - in: query
          name: label
          required: false
          description: |
            Repeated key:value pairs. AND-combined across keys. Same key
            with different values rejected as unsatisfiable.
          schema:
            type: array
            items:
              type: string
              pattern: "^[a-z][a-z0-9_-]{0,62}:[a-z][a-z0-9_-]{0,62}$"
          style: form
          explode: true
          example: ["env:prod", "team:platform"]
      responses:
        "200":
          description: List of enriched monitors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EnrichedMonitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    post:
      tags: [Monitors]
      summary: Create monitor
      operationId: createMonitor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMonitorRequest"
      responses:
        "201":
          description: Monitor created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/monitors/{monitorID}:
    parameters:
      - name: monitorID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Monitor UUID

    get:
      tags: [Monitors]
      summary: Get monitor
      operationId: getMonitor
      responses:
        "200":
          description: Monitor details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    put:
      tags: [Monitors]
      summary: Update monitor
      description: Only provided fields are updated; omitted fields remain unchanged.
      operationId: updateMonitor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMonitorRequest"
      responses:
        "200":
          description: Monitor updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      tags: [Monitors]
      summary: Delete monitor
      operationId: deleteMonitor
      responses:
        "204":
          description: Monitor deleted
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/monitors/{monitorID}/checks:
    parameters:
      - name: monitorID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Monitor UUID

    get:
      tags: [Monitors]
      summary: List check results
      description: Returns paginated check results for a monitor, ordered by most recent first.
      operationId: getMonitorChecks
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
          description: Number of results to return
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Offset for pagination
      responses:
        "200":
          description: List of check results
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CheckResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/monitors/{monitorID}/stats:
    parameters:
      - name: monitorID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Monitor UUID

    get:
      tags: [Monitors]
      summary: Get monitor statistics
      description: Returns 30-day uptime percentage, average response time, check count, and incident count.
      operationId: getMonitorStats
      responses:
        "200":
          description: Monitor statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonitorStats"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/monitors/{monitorID}/uptime:
    parameters:
      - name: monitorID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Monitor UUID

    get:
      tags: [Monitors]
      summary: Get daily uptime data
      description: Returns per-day uptime status for the specified number of days.
      operationId: getMonitorUptime
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 90
          description: Number of days of uptime history
      responses:
        "200":
          description: Daily uptime entries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DailyUptimeEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/alert-channels:
    get:
      tags: [Alert Channels]
      summary: List alert channels
      operationId: listAlertChannels
      responses:
        "200":
          description: List of alert channels
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AlertChannel"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    post:
      tags: [Alert Channels]
      summary: Create alert channel
      operationId: createAlertChannel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertChannelRequest"
      responses:
        "201":
          description: Alert channel created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertChannel"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/alert-channels/{channelID}:
    parameters:
      - name: channelID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Alert channel UUID

    get:
      tags: [Alert Channels]
      summary: Get alert channel
      operationId: getAlertChannel
      responses:
        "200":
          description: Alert channel details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertChannel"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    put:
      tags: [Alert Channels]
      summary: Update alert channel
      operationId: updateAlertChannel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AlertChannelRequest"
      responses:
        "200":
          description: Alert channel updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertChannel"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      tags: [Alert Channels]
      summary: Delete alert channel
      operationId: deleteAlertChannel
      responses:
        "204":
          description: Alert channel deleted
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/status-pages:
    get:
      tags: [Status Pages]
      summary: List status pages
      operationId: listStatusPages
      responses:
        "200":
          description: List of status pages
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/StatusPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    post:
      tags: [Status Pages]
      summary: Create status page
      operationId: createStatusPage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusPageRequest"
      responses:
        "201":
          description: Status page created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/status-pages/{pageID}:
    parameters:
      - name: pageID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Status page UUID

    get:
      tags: [Status Pages]
      summary: Get status page
      operationId: getStatusPage
      responses:
        "200":
          description: Status page details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    put:
      tags: [Status Pages]
      summary: Update status page
      operationId: updateStatusPage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StatusPageRequest"
      responses:
        "200":
          description: Status page updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      tags: [Status Pages]
      summary: Delete status page
      operationId: deleteStatusPage
      responses:
        "204":
          description: Status page deleted
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/status-pages/{pageID}/verify-domain:
    parameters:
      - name: pageID
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Status page UUID
    post:
      tags: [Status Pages]
      summary: Verify the page's custom domain via CNAME lookup
      description: |
        Resolves the CNAME of the status page's `custom_domain` and confirms
        it points at the configured target (default `cname.vartio.dev`). On
        success the page becomes live on the custom domain. On failure the
        page remains gated — public requests against an unverified domain
        fall through to a 404. Returns 200 in both success and failure
        cases; the response body's `verified` flag tells the caller which.
      operationId: verifyStatusPageDomain
      responses:
        "200":
          description: Verification attempt completed (success or failure)
          content:
            application/json:
              schema:
                type: object
                required: [verified, domain, error, last_check_at]
                properties:
                  verified:
                    type: boolean
                  domain:
                    type: string
                  verified_at:
                    type: string
                    format: date-time
                    description: Set only when verified=true
                  last_check_at:
                    type: string
                    format: date-time
                  error:
                    type: string
                    description: Empty string on success; human-readable failure reason otherwise
        "400":
          description: Status page has no custom_domain configured
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/v1/services:
    get:
      tags: [Services]
      summary: List logical services in the organization
      description: |
        A "service" groups one or more monitors into a logical unit
        (e.g. "Checkout API" comprising the load balancer, the order
        service, and the database health endpoint). Returned entries are
        enriched with current rolled-up status, uptime, and the contained
        monitors.

        Optional `label` query parameter filters by Prometheus-style
        labels (same semantics as the monitors list).

        Requires the `services:read` API key permission.
      operationId: listServices
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: label
          required: false
          schema:
            type: array
            items:
              type: string
              pattern: "^[a-z][a-z0-9_-]{0,62}:[a-z][a-z0-9_-]{0,62}$"
          style: form
          explode: true
          example: ["env:prod"]
      responses:
        "200":
          description: Services
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EnrichedService"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Services]
      summary: Create a service
      description: |
        Optionally attach a list of monitor IDs in `monitor_ids` — those
        monitors are linked to the service in a single call. Requires the
        `services:write` API key permission.
      operationId: createService
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ServiceRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnrichedService"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/services/{serviceID}:
    parameters:
      - in: path
        name: serviceID
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Services]
      summary: Get a service with rolled-up status and contained monitors
      operationId: getService
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Service
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnrichedService"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Services]
      summary: Update a service
      description: |
        Pass `monitor_ids` to replace the set of attached monitors. Omit it
        to leave the membership unchanged. Pass an empty array to detach
        all monitors.
      operationId: updateService
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ServiceRequest"
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnrichedService"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Services]
      summary: Delete a service
      description: Removes the grouping; does not delete the underlying monitors.
      operationId: deleteService
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /.well-known/vartio:
    get:
      tags: [Discovery]
      summary: Service discovery document
      description: |
        One-shot JSON describing the API's auth methods, route surface, rate
        limits, and the canonical problem-details base URI. Public; no auth
        required. Cached for 5 minutes. Designed for AI agents to fetch on
        first contact.
      operationId: getDiscovery
      responses:
        "200":
          description: Discovery document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscoveryDocument"

  /api/v1/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys for the current organization
      description: |
        Returns metadata only — the raw key is returned ONCE on creation
        (`POST /api/v1/api-keys`) and never again.
      operationId: listAPIKeys
      security:
        - bearerAuth: []
      responses:
        "200":
          description: API keys
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/APIKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [API Keys]
      summary: Create an API key
      description: |
        Returns the raw key (vk_...) once. Store it now; the server only
        keeps a SHA256 hash. Permissions follow the `domain:action` pattern
        (e.g. `monitors:read`) or the wildcard `*`.
      operationId: createAPIKey
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAPIKeyRequest"
      responses:
        "201":
          description: Created — raw key included once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateAPIKeyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/api-keys/{keyID}:
    delete:
      tags: [API Keys]
      summary: Revoke an API key
      operationId: deleteAPIKey
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: keyID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Revoked
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/incidents/{incidentID}/acknowledge:
    post:
      tags: [Incidents]
      summary: Acknowledge an open incident
      operationId: acknowledgeIncident
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Updated incident
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Incident"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/incidents/{incidentID}/postmortem:
    put:
      tags: [Incidents]
      summary: Set or update an incident postmortem
      operationId: updateIncidentPostmortem
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentID
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [postmortem]
              properties:
                postmortem:
                  type: string
                  description: Markdown-formatted postmortem text.
      responses:
        "200":
          description: Updated incident
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Incident"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/incidents/{incidentID}/triage:
    get:
      tags: [Incidents]
      summary: AI-generated triage for an incident
      description: |
        Runs DNS, SSL, TCP, and HTTP diagnostics against the affected
        monitor and asks Claude Sonnet to summarize likely cause, severity,
        and recommended actions. Falls back to rule-based output if the
        Anthropic API is unavailable. Cached for 5 minutes.
      operationId: triageIncident
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: incidentID
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Triage result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TriageResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /public/services/{slug}:
    get:
      tags: [Public Services]
      summary: Get a public "Is X Down?" service entry
      operationId: getPublicService
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Service entry
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicService"
        "404":
          $ref: "#/components/responses/NotFound"

  /public/services/{slug}/history:
    get:
      tags: [Public Services]
      summary: Recent status history for a public service
      operationId: getPublicServiceHistory
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
      responses:
        "200":
          description: History
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PublicServiceHistoryEntry"

  /public/services/{slug}/live-check:
    get:
      tags: [Public Services]
      summary: Run a fresh check against the service right now
      description: |
        Performs an HTTP probe and (in parallel) fetches the official
        status page. 30-second cache with request deduplication.
      operationId: liveCheckPublicService
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Live check result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveCheckResult"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /public/services/{slug}/triage:
    get:
      tags: [Public Services]
      summary: AI-generated triage for a public "Is X Down?" service
      description: |
        Same diagnostic+triage flow as the authenticated incident triage,
        but for the public service catalogue. Rate-limited to 3/min/IP.
      operationId: triagePublicService
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Triage result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TriageResult"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /public/check:
    get:
      tags: [Public Services]
      summary: One-shot status check for an arbitrary URL
      description: |
        Quick HTTP check for ad-hoc URLs not in the public catalogue.
        Rate-limited to 10/min/IP.
      operationId: checkArbitraryURL
      parameters:
        - in: query
          name: url
          required: true
          schema:
            type: string
            format: uri
      responses:
        "200":
          description: Check result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveCheckResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or vk_*
      description: |
        Two accepted Bearer tokens:

        - **JWT** (browser session): `Authorization: Bearer eyJ...`
          Issued by `POST /api/v1/auth/login`, contains `sub` (user_id) and
          `org_id` claims, expires after 24h.
        - **API key** (machine clients, AI agents): `Authorization: Bearer vk_...`
          Created in the dashboard at https://vartio.dev/settings
          or via `POST /api/v1/api-keys`. Scoped to a single organization.
          Recommended for any automated workflow.

  responses:
    BadRequest:
      description: Bad request
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Internal server error
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ValidationFailed:
      description: One or more request fields failed validation (RFC 9457)
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"
    RateLimited:
      description: Rate limit exceeded — retry after the period in `Retry-After`
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Maximum requests allowed in the current window
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining in the current window
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp when the window resets
        Retry-After:
          schema:
            type: integer
          description: Seconds until the next request will be accepted
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/Problem"

  schemas:
    Error:
      type: object
      description: |
        Legacy error envelope. New responses are `application/problem+json`
        and use the `Problem` schema instead. The `error` field is preserved
        as an alias of `Problem.detail` for backward compatibility.
      required: [error]
      properties:
        error:
          type: string
          example: invalid request body

    Problem:
      type: object
      description: |
        RFC 9457 problem-details body served as `application/problem+json`.
        Always returned by error responses (4xx, 5xx). The `error` field
        mirrors `detail` so legacy clients keep working.
      required: [type, title, status, detail, code, error]
      properties:
        type:
          type: string
          format: uri
          description: Canonical URI identifying the problem class.
          example: https://vartio.dev/problems/vartio.validation.failed
        title:
          type: string
          description: Short, human-readable summary that does not vary by occurrence.
          example: Validation Failed
        status:
          type: integer
          description: HTTP status code that mirrors the response status.
          example: 422
        detail:
          type: string
          description: Request-specific human-readable explanation.
          example: validation failed
        instance:
          type: string
          format: uri-reference
          description: URI reference to the specific occurrence (the request URI).
          example: /api/v1/auth/register
        code:
          type: string
          description: Stable Vartio identifier for programmatic handling.
          example: vartio.validation.failed
        error:
          type: string
          description: Alias of `detail` preserved for backward compatibility.
          example: validation failed
        fields:
          type: object
          description: Per-field validation errors (only on 422 responses).
          additionalProperties:
            type: string
          example:
            email: required
            password: must be at least 8 characters

    DiscoveryDocument:
      type: object
      description: Returned by GET /.well-known/vartio.
      required: [service, version, auth_methods, endpoints, rate_limits, problem_type_base]
      properties:
        service:
          type: string
          example: vartio
        version:
          type: string
          example: 1.50.0
        description:
          type: string
        auth_methods:
          type: array
          items:
            type: object
            required: [id, header, scheme, example, description]
            properties:
              id:
                type: string
                example: bearer_api_key
              header:
                type: string
                example: Authorization
              scheme:
                type: string
                example: Bearer
              example:
                type: string
                example: "Authorization: Bearer vk_..."
              description:
                type: string
        endpoints:
          type: object
          properties:
            openapi: { type: string }
            swagger_ui: { type: string }
            llms_txt: { type: string }
            llms_full: { type: string }
            api_base: { type: string }
            public_status_pages: { type: string }
        rate_limits:
          type: array
          items:
            type: object
            required: [class, limit, window_seconds, keyed_by, description]
            properties:
              class: { type: string, example: api_protected }
              limit: { type: integer, example: 100 }
              window_seconds: { type: integer, example: 60 }
              keyed_by: { type: string, example: api_key_or_org }
              description: { type: string }
        mcp:
          nullable: true
          type: object
          description: Forward-compatibility stub. `null` until MCP server ships.
          properties:
            endpoint: { type: string }
            auth_hint: { type: string }
        problem_type_base:
          type: string
          format: uri
          example: https://vartio.dev/problems

    APIKey:
      type: object
      description: API key metadata. The raw key is only ever returned by `POST /api/v1/api-keys`.
      properties:
        id: { type: string, format: uuid }
        name: { type: string, example: "deploy-bot" }
        prefix: { type: string, example: "vk_a1b2c3" }
        permissions:
          type: array
          items: { type: string }
          example: ["monitors:read", "monitors:write", "alerts:read"]
        last_used_at: { type: string, format: date-time, nullable: true }
        expires_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    CreateAPIKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Human-readable name; shown in the dashboard.
          example: "deploy-bot"
        permissions:
          type: array
          description: |
            Domain:action pairs. Defaults to a broad read+write set covering
            monitors, alerts, status pages, services, incidents, and
            maintenance. Use `*` for full access (not recommended).
          items: { type: string }
          example: ["monitors:read", "alerts:read"]
        expires_in:
          type: string
          description: One of `30d`, `90d`, `1y`, `never`.
          enum: [30d, 90d, 1y, never]
          example: "90d"

    CreateAPIKeyResponse:
      allOf:
        - $ref: "#/components/schemas/APIKey"
        - type: object
          required: [key]
          properties:
            key:
              type: string
              description: The raw API key. Shown ONCE; store it now.
              example: "vk_aBcDeFgHiJ..."

    Incident:
      type: object
      properties:
        id: { type: string, format: uuid }
        org_id: { type: string, format: uuid }
        monitor_id: { type: string, format: uuid }
        status: { type: string, enum: [open, acknowledged, resolved] }
        cause: { type: string }
        started_at: { type: string, format: date-time }
        resolved_at: { type: string, format: date-time, nullable: true }
        acknowledged_at: { type: string, format: date-time, nullable: true }
        acknowledged_by: { type: string, format: uuid, nullable: true }
        postmortem: { type: string }
        monitor_name: { type: string }

    TriageResult:
      type: object
      required: [summary, severity]
      properties:
        summary:
          type: string
          description: One-paragraph human-readable summary.
        likely_cause:
          type: string
        severity:
          type: string
          enum: [critical, major, minor, investigating]
        recommendations:
          type: array
          items: { type: string }
        diagnostics:
          type: object
          description: Raw DNS / SSL / TCP / HTTP probe results used to build the summary.
        cached:
          type: boolean
          description: True if served from the 5-minute triage cache.

    PublicService:
      type: object
      properties:
        slug: { type: string, example: github }
        name: { type: string, example: GitHub }
        url: { type: string, format: uri }
        category: { type: string }
        current_status: { type: string, enum: [up, down, degraded, unknown] }
        last_checked_at: { type: string, format: date-time }

    PublicServiceHistoryEntry:
      type: object
      properties:
        checked_at: { type: string, format: date-time }
        status: { type: string }
        response_time_ms: { type: integer }

    Service:
      type: object
      description: A logical grouping of monitors within an organization.
      properties:
        id: { type: string, format: uuid }
        org_id: { type: string, format: uuid }
        name: { type: string, example: "Checkout API" }
        description: { type: string }
        sort_order:
          type: integer
          description: Display order on the dashboard / status page; lower values first.
          example: 0
        labels:
          $ref: "#/components/schemas/Labels"
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    EnrichedService:
      allOf:
        - $ref: "#/components/schemas/Service"
        - type: object
          properties:
            status:
              type: string
              enum: [up, down, degraded, unknown]
              description: Worst current status across the contained monitors.
            uptime_percent:
              type: number
              format: float
              description: 30-day rolled-up uptime across all contained monitors.
            monitor_count:
              type: integer
            down_count:
              type: integer
            degraded_count:
              type: integer
            monitors:
              type: array
              description: Contained monitors with current status. Secrets redacted.
              items:
                $ref: "#/components/schemas/Monitor"

    ServiceRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: "Checkout API"
        description:
          type: string
        sort_order:
          type: integer
          example: 0
        monitor_ids:
          type: array
          description: |
            On create: monitors to attach to the new service.
            On update: full replacement of the attached set; omit to leave
            membership unchanged; pass `[]` to detach all monitors.
          items:
            type: string
            format: uuid
        labels:
          allOf:
            - $ref: "#/components/schemas/Labels"
          description: |
            On update, omit to leave labels unchanged; pass `{}` to clear.

    Labels:
      type: object
      description: |
        Prometheus-style key/value pairs. Keys and values must match
        `^[a-z][a-z0-9_-]{0,62}$`. Keys starting with `vartio.` are
        reserved for system-set labels. Maximum 16 labels per entity.
      additionalProperties:
        type: string
      maxProperties: 16
      example:
        env: prod
        team: platform
        tier: critical

    LiveCheckResult:
      type: object
      properties:
        status: { type: string, enum: [up, down, degraded] }
        response_time_ms: { type: integer }
        status_code: { type: integer, nullable: true }
        outage_reason:
          type: string
          nullable: true
          description: Reason scraped from the official status page, if any.
        sources:
          type: array
          items: { type: string }
        cached:
          type: boolean
          description: "Indicates `X-Cache: HIT` (30s cache)."

    MessageResponse:
      type: object
      properties:
        message:
          type: string
          example: Operation completed successfully

    RegisterRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: user@example.com
        password:
          type: string
          minLength: 8
          example: securepassword123
        name:
          type: string
          example: Jane Doe
        org_name:
          type: string
          description: Organization name. Defaults to "{name}'s Organization" if omitted.
          example: Acme Corp

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: user@example.com
        password:
          type: string
          example: securepassword123

    ForgotPasswordRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
          example: user@example.com

    ResetPasswordRequest:
      type: object
      required: [token, new_password]
      properties:
        token:
          type: string
          description: Reset token from the password reset email
        new_password:
          type: string
          minLength: 8

    AuthResponse:
      type: object
      properties:
        token:
          type: string
          description: JWT bearer token
        user:
          $ref: "#/components/schemas/User"

    MeResponse:
      type: object
      properties:
        user:
          $ref: "#/components/schemas/User"
        organizations:
          type: array
          items:
            $ref: "#/components/schemas/Organization"

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Organization:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        plan:
          type: string
          enum: [free, pro, enterprise]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Monitor:
      type: object
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
          example: Production API
        type:
          type: string
          enum: [http, ssl, dns, ping]
        url:
          type: string
          description: Target URL, hostname, domain, or IP depending on monitor type
          example: https://api.example.com/health
        interval_seconds:
          type: integer
          description: Check interval in seconds
          example: 300
        timeout_ms:
          type: integer
          description: Check timeout in milliseconds
          example: 10000
        expected_status:
          type: integer
          description: Expected HTTP status code (HTTP monitors only)
          example: 200
        regions:
          type: array
          items:
            type: string
          example: ["eu-frankfurt", "eu-amsterdam", "eu-helsinki"]
        enabled:
          type: boolean
        labels:
          $ref: "#/components/schemas/Labels"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    EnrichedMonitor:
      allOf:
        - $ref: "#/components/schemas/Monitor"
        - type: object
          properties:
            current_status:
              type: string
              enum: [up, down, degraded, unknown]
              description: Status from the most recent check
            uptime_percent:
              type: number
              format: double
              description: 30-day uptime percentage
              example: 99.95
            last_response_ms:
              type: integer
              description: Response time of the most recent check in milliseconds
              example: 142

    CreateMonitorRequest:
      type: object
      required: [name, url]
      properties:
        name:
          type: string
          example: Production API
        type:
          type: string
          enum: [http, ssl, dns, ping]
          default: http
        url:
          type: string
          description: |
            Target to monitor. Format depends on type:
            - http: Full URL (https://example.com)
            - ssl: Hostname (example.com)
            - dns: Domain name (example.com)
            - ping: Hostname or IP (example.com or 1.2.3.4)
          example: https://api.example.com/health
        interval_seconds:
          type: integer
          description: Check interval in seconds
          default: 300
        timeout_ms:
          type: integer
          description: Check timeout in milliseconds
          default: 10000
        expected_status:
          type: integer
          description: Expected HTTP status code (HTTP monitors only)
          default: 200
        regions:
          type: array
          items:
            type: string
          default: ["eu-frankfurt", "eu-amsterdam", "eu-helsinki"]
        labels:
          $ref: "#/components/schemas/Labels"

    CheckResult:
      type: object
      properties:
        id:
          type: string
          format: uuid
        monitor_id:
          type: string
          format: uuid
        region:
          type: string
          example: eu-helsinki
        status:
          type: string
          enum: [up, down, degraded]
        response_time_ms:
          type: integer
          example: 142
        status_code:
          type: integer
          description: HTTP status code (0 for non-HTTP checks or on error)
          example: 200
        error_message:
          type: string
          description: Error details when status is down or degraded
        checked_at:
          type: string
          format: date-time

    MonitorStats:
      type: object
      properties:
        uptime_percentage:
          type: number
          format: double
          description: 30-day uptime percentage
          example: 99.95
        avg_response_time:
          type: number
          format: double
          description: 30-day average response time in milliseconds
          example: 187.5
        checks_count:
          type: integer
          format: int64
          description: Total checks in the last 30 days
          example: 8640
        incidents_count:
          type: integer
          format: int64
          description: Total incidents in the last 30 days
          example: 2

    DailyUptimeEntry:
      type: object
      properties:
        date:
          type: string
          format: date-time
          description: Day (date portion)
        status:
          type: string
          enum: [up, down, degraded, no_data]
          description: Worst status observed that day

    AlertChannel:
      type: object
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        type:
          type: string
          enum: [email, slack, webhook, discord]
        name:
          type: string
          example: Ops Team Email
        config:
          type: object
          description: |
            Channel-specific configuration. Structure depends on type:
            - email: `{"email": "ops@example.com"}`
            - slack: `{"webhook_url": "https://hooks.slack.com/..."}`
            - discord: `{"webhook_url": "https://discord.com/api/webhooks/..."}`
            - webhook: `{"url": "https://example.com/webhook", "secret": "optional-secret"}`
          additionalProperties: true
        enabled:
          type: boolean
        monitor_label_filter:
          allOf:
            - $ref: "#/components/schemas/Labels"
          description: |
            If non-empty, the channel only fires for monitors whose labels
            contain every key/value in this map (PostgreSQL `@>` containment,
            AND semantics). Empty/omitted = no filter; channel fires for all
            monitors in the org (current behavior).

    AlertChannelRequest:
      type: object
      required: [type, name]
      properties:
        type:
          type: string
          enum: [email, slack, webhook, discord]
        name:
          type: string
          example: Ops Team Slack
        config:
          type: object
          description: Channel-specific configuration (see AlertChannel.config)
          additionalProperties: true
        enabled:
          type: boolean
          default: true
        monitor_label_filter:
          allOf:
            - $ref: "#/components/schemas/Labels"
          description: |
            Optional label filter. On update, omit to leave unchanged;
            pass `{}` to clear an existing filter.

    StatusPage:
      type: object
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
          example: Acme Status
        slug:
          type: string
          example: acme-status
        custom_domain:
          type: string
          example: status.acme.com
        custom_domain_verified:
          type: boolean
          description: |
            True when the page's custom_domain has been confirmed via CNAME
            lookup (POST /status-pages/{id}/verify-domain). Public requests
            against the domain are only served while this is true. Changing
            custom_domain via PUT resets this to false until re-verified.
        custom_domain_verified_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent successful verification. Null when never verified.
        custom_domain_last_check_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent verification attempt (success or failure). Null when never checked.
        custom_domain_check_error:
          type: string
          description: Human-readable failure reason from the last verification attempt; empty on success.
        monitor_ids:
          type: array
          items:
            type: string
            format: uuid
        service_ids:
          type: array
          description: |
            Services attached to this page. Each service expands at
            render time into its own group, populated from the live
            service-monitor membership — adding a monitor to a service
            automatically surfaces it on the page.
          items:
            type: string
            format: uuid
        branding:
          type: object
          description: Custom branding options (logo URL, colors, etc.)
          additionalProperties: true
        public:
          type: boolean

    StatusPageRequest:
      type: object
      required: [name, slug]
      properties:
        name:
          type: string
          example: Acme Status
        slug:
          type: string
          example: acme-status
        custom_domain:
          type: string
        monitor_ids:
          type: array
          items:
            type: string
            format: uuid
        service_ids:
          type: array
          items:
            type: string
            format: uuid
        branding:
          type: object
          additionalProperties: true
        public:
          type: boolean
          default: true

    PublicStatusPageResponse:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
        branding:
          type: object
          additionalProperties: true
        overall_status:
          type: string
          enum: [operational, partial_outage, major_outage]
        monitors:
          type: array
          description: Ungrouped monitors directly attached to the page.
          items:
            $ref: "#/components/schemas/PublicStatusMonitor"
        services:
          type: array
          description: |
            Service-derived component groups. Each linked service is
            rendered as its own group, populated from the live
            service-monitor membership at request time.
          items:
            $ref: "#/components/schemas/PublicStatusComponentGroup"
        component_groups:
          type: array
          description: User-defined component groups.
          items:
            $ref: "#/components/schemas/PublicStatusComponentGroup"

    PublicStatusMonitor:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        current_status:
          type: string
          enum: [up, down, degraded, unknown]
        uptime_percent:
          type: number
          format: double
          description: 90-day uptime percentage
        daily_uptime:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              status:
                type: string

    PublicStatusComponentGroup:
      type: object
      properties:
        id:
          type: string
          description: |
            For service-derived groups this is `service:<uuid>`; for
            user-defined groups it's the group's own ID.
        name:
          type: string
        sort_order:
          type: integer
        collapsed_by_default:
          type: boolean
        status:
          type: string
          enum: [operational, partial_outage, major_outage]
        uptime_percent:
          type: number
          format: double
        monitors:
          type: array
          items:
            $ref: "#/components/schemas/PublicStatusMonitor"
        incidents:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              title:
                type: string
              status:
                type: string
              updates:
                type: array
                items:
                  type: object
                  properties:
                    status:
                      type: string
                    message:
                      type: string
                    timestamp:
                      type: string
                      format: date-time
