openapi: 3.1.0
info:
  title: GetWhatChanged HTTP API
  version: "1.0"
  license:
    name: Proprietary
    url: https://getwhatchanged.com/legal/terms
  description: |
    **API version:** **1.0**  
    **Production base URL:** `https://api.getwhatchanged.com/api`

    Public HTTP API under the `/api` prefix for **server-to-server** use with a **server API key**.

    **Server API keys** (Bearer `gwc_sk_…`) are **included with the Sentinel plan**. Create and revoke keys in
    GetWhatChanged (**Settings → Server API keys**), then call the endpoints below from your services using
    `Authorization: Bearer gwc_sk_<secret>` (prefix always `gwc_sk_`).

    **Response envelope:** every JSON body is either `{ "success": true, "data": … }` or
    `{ "success": false, "error": { "code", "message", … }, "meta"?: { "requestId" } }`.

    **Rate limits:** Requests are throttled per server API key. Limit details depend on the operation and your plan.

    **Security:** Treat all response bodies as untrusted; call this API over **HTTPS** only.

    **Contact:** hello@getwhatchanged.com  
    **Product:** https://getwhatchanged.com

x-tagGroups:
  - name: API
    tags: [Monitors, Changes, Analytics]

servers:
  - url: https://api.getwhatchanged.com/api
    description: Production

tags:
  - name: Monitors
    description: Monitors, pages, runs, snapshots, compare, and per-monitor analytics.
  - name: Changes
    description: Workspace-wide change feed and single change detail.
  - name: Analytics
    description: Overview metrics across monitors.

security:
  - BearerAuth: []

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Send your server API key in the standard `Authorization` header. Keys start with `gwc_sk_`; use the full
        value exactly as shown once when the key is created in GetWhatChanged.

  parameters:
    MonitorId:
      name: monitorId
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/IdParam"
      description: Monitor identifier (UUID or opaque id string from list monitors).
    ChangeId:
      name: id
      in: path
      required: true
      schema:
        $ref: "#/components/schemas/IdParam"
      description: Change record identifier.

  schemas:
    IdParam:
      type: string
      maxLength: 64
      pattern: "^[a-zA-Z0-9-]+$"
      description: Non-empty alphanumeric id (hyphens allowed).

    SuccessEnvelope:
      type: object
      required: [success, data]
      properties:
        success:
          const: true
        data:
          type: object
          additionalProperties: true

    ErrorBody:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          maxLength: 96
          description: |
            Machine-readable error code. Treat as the stable identifier for handling; **do not** match on
            `message` text, which may change without notice.
        message:
          type: string
          description: Human-readable summary; wording is not guaranteed stable across releases.
        details:
          description: Optional extra context; shape is not part of the public contract.

    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success:
          const: false
        error:
          $ref: "#/components/schemas/ErrorBody"
        meta:
          type: object
          properties:
            requestId:
              type: string

    CreateMonitorRequest:
      type: object
      required: [name, websiteUrl, frequencyMinutes]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        websiteUrl:
          type: string
          maxLength: 2048
          description: |
            `http` or `https` URL (may omit scheme; `https` is assumed).  
            Some non-public URL targets may be rejected.
        frequencyMinutes:
          type: integer
          enum: [15, 30, 60, 180, 360, 720, 1440]
          default: 60

    AddPageRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          maxLength: 2048
          description: Page URL under the monitor (same rules as `websiteUrl`).
        label:
          type: string
          maxLength: 120
          default: ""
          description: Optional display label.

    RangeQuery:
      type: string
      enum: [24h, 7d, 30d]
      default: 7d

  responses:
    Unauthorized:
      description: Missing or invalid `Authorization` bearer (server API key).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          examples:
            unauthorized:
              value:
                success: false
                error:
                  code: UNAUTHORIZED
                  message: "A valid server API key is required"
                meta:
                  requestId: req_example
    ForbiddenPlan:
      description: Active plan with server API access required (included with the Sentinel plan).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    ValidationError:
      description: Invalid query, path, or body.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          examples:
            validation:
              value:
                success: false
                error:
                  code: VALIDATION_ERROR
                  message: "range must be one of: 24h, 7d, 30d"
                meta:
                  requestId: req_example

paths:
  /monitors:
    get:
      operationId: listMonitors
      tags: [Monitors]
      summary: List monitors
      description: Monitors for the workspace associated with this server API key.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createMonitor
      tags: [Monitors]
      summary: Create monitor
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateMonitorRequest" }
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ForbiddenPlan"

  /monitors/latest-runs:
    get:
      operationId: listMonitorsLatestRuns
      tags: [Monitors]
      summary: Latest run per monitor
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /monitors/{monitorId}/timeline:
    get:
      operationId: getMonitorTimeline
      tags: [Monitors]
      summary: Timeline / snapshots feed
      parameters:
        - $ref: "#/components/parameters/MonitorId"
        - name: pageId
          in: query
          required: false
          schema: { $ref: "#/components/schemas/IdParam" }
          description: When set, restricts the feed to a single page.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /monitors/{monitorId}/pages:
    get:
      operationId: listMonitorPages
      tags: [Monitors]
      summary: List pages for a monitor
      parameters:
        - $ref: "#/components/parameters/MonitorId"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: addMonitorPage
      tags: [Monitors]
      summary: Add page to monitor
      parameters:
        - $ref: "#/components/parameters/MonitorId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddPageRequest" }
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/ForbiddenPlan"

  /monitors/{monitorId}/runs:
    get:
      operationId: listMonitorRuns
      tags: [Monitors]
      summary: Run history
      parameters:
        - $ref: "#/components/parameters/MonitorId"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /monitors/{monitorId}/analytics:
    get:
      operationId: getMonitorAnalytics
      tags: [Monitors]
      summary: Per-monitor analytics
      parameters:
        - $ref: "#/components/parameters/MonitorId"
        - name: range
          in: query
          schema: { $ref: "#/components/schemas/RangeQuery" }
          description: Rolling window (default `7d`).
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /monitors/{monitorId}/snapshots:
    get:
      operationId: listMonitorSnapshots
      tags: [Monitors]
      summary: List snapshots
      parameters:
        - $ref: "#/components/parameters/MonitorId"
        - name: pageId
          in: query
          required: false
          schema: { $ref: "#/components/schemas/IdParam" }
          description: Filter to a single page when provided.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /monitors/{monitorId}/compare:
    get:
      operationId: compareMonitorSnapshots
      tags: [Monitors]
      summary: Compare two snapshots
      parameters:
        - $ref: "#/components/parameters/MonitorId"
        - name: fromSnapshotId
          in: query
          required: true
          schema: { $ref: "#/components/schemas/IdParam" }
        - name: toSnapshotId
          in: query
          required: true
          schema: { $ref: "#/components/schemas/IdParam" }
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /changes:
    get:
      operationId: listChanges
      tags: [Changes]
      summary: List changes
      parameters:
        - name: monitorId
          in: query
          required: false
          schema: { $ref: "#/components/schemas/IdParam" }
        - name: pageId
          in: query
          required: false
          schema: { $ref: "#/components/schemas/IdParam" }
          description: If set, `monitorId` must also be set.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /changes/{id}:
    get:
      operationId: getChange
      tags: [Changes]
      summary: Get single change
      parameters:
        - $ref: "#/components/parameters/ChangeId"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /analytics/overview:
    get:
      operationId: getAnalyticsOverview
      tags: [Analytics]
      summary: Workspace overview analytics
      parameters:
        - name: range
          in: query
          schema: { $ref: "#/components/schemas/RangeQuery" }
          description: Rolling window (default `7d`).
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
