openapi: 3.1.0
info:
  title: Probecast API
  version: "1.0"
  description: |
    Monitoring and monthly client reports for the projects you ship.

    Authenticate every request with an API key created in
    Settings → API Keys: `Authorization: Bearer pb_live_…`.
    Read keys list monitors and incidents; write keys can also create
    and delete monitors and ping heartbeats. Rate limits: 120 req/min
    per key for reads and 30 req/min per key for writes (HTTP 429 when
    exceeded).
servers:
  - url: https://api.probecast.io/v1
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "API key from Settings → API Keys (pb_live_…)"
  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          properties:
            message: { type: string }
            code: { type: string }
    Monitor:
      type: object
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [website, heartbeat, custom_api] }
        name: { type: string }
        url: { type: [string, "null"] }
        enabled: { type: boolean }
        client:
          type: [object, "null"]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
paths:
  /me:
    get:
      summary: Account, plan and remaining quotas
      description: Call this before creating monitors to know what fits.
      responses:
        "200":
          description: Plan, key scope and used/limit per quota.
        "401": { description: Invalid or missing API key. }
  /clients:
    get:
      summary: List clients (the projects you group monitors under)
      responses:
        "200": { description: "Array of { id, name, slug, paused }." }
  /monitors:
    get:
      summary: List monitors with their client
      responses:
        "200":
          description: Array of Monitor.
    post:
      summary: Create a monitor (write key)
      description: |
        Send Idempotency-Key to make retries safe: the same key returns
        the original response instead of creating a duplicate.
        Pass `client` (a name — created if missing) or `client_id` to
        group the monitor under a client.
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 128 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, name]
              properties:
                type: { type: string, enum: [website, heartbeat, custom_api] }
                name: { type: string, maxLength: 200 }
                url: { type: string, description: "Required for website and custom_api. Public https URLs only." }
                client: { type: string, description: "Client name — created if it does not exist yet." }
                client_id: { type: string, format: uuid }
                interval_minutes: { type: integer, description: "Heartbeat only. Default 60." }
                grace_minutes: { type: integer, description: "Heartbeat only. Default 10." }
                method: { type: string, enum: [GET, HEAD, POST], description: "custom_api only. Default GET." }
                expected_status: { type: integer, description: "custom_api only. Default 200." }
      responses:
        "201":
          description: "Created. Heartbeats include ping_url — call it from your cron."
        "400": { description: Invalid input (including internal/private URLs). }
        "403": { description: Read-only key (code READ_ONLY_KEY) or a full plan quota (code QUOTA / CLIENT_QUOTA). }
        "404": { description: client_id does not exist in this workspace. }
        "429": { description: Rate limited (30 writes per minute per key). }
  /monitors/{id}:
    delete:
      summary: Delete a monitor (write key)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: Deleted. }
        "404": { description: Not found (or not yours). }
  /incidents:
    get:
      summary: Ongoing incidents (the pull your agent reads)
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [ongoing, recent] }
          description: "recent also includes incidents resolved in the last 24h."
      responses:
        "200":
          description: "{ incidents: […], websites_down: […] }"
  /status/{slug}:
    get:
      summary: Public catalog status (APIs and MCP servers)
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        "200": { description: "{ name, slug, status, category, kind, page }." }
        "404": { description: Unknown slug. }
  /heartbeats/{id}/ping:
    post:
      summary: Authenticated heartbeat ping (write key)
      description: |
        Marks the heartbeat up and resolves any open incident — state
        changes, so it needs a write key. Crons normally use the public
        ping_url returned at creation instead; no key travels there.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: "{ received: true, enabled: boolean } — a disabled monitor acknowledges without changing state." }
        "403": { description: Read-only key (code READ_ONLY_KEY). }
        "404": { description: Not found (or not yours). }
