openapi: 3.1.0

info:
  title: Tickstem API
  version: "1"
  description: |
    Tickstem provides developer infrastructure primitives — cron scheduling,
    uptime monitoring, and email verification — accessible via a single API key.

    All API requests require an API key issued from the dashboard.
  contact:
    email: hi@tickstem.dev
  license:
    name: Proprietary

servers:
  - url: https://api.tickstem.dev/v1
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key from https://app.tickstem.dev/dashboard/keys

  schemas:
    Job:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        user_id:
          type: string
          readOnly: true
        name:
          type: string
          example: "Send digest email"
        description:
          type: string
          example: "Daily digest sent to all active users"
        schedule:
          type: string
          description: Standard 5-field cron expression (minute hour dom month dow)
          example: "0 9 * * 1-5"
        endpoint:
          type: string
          format: uri
          example: "https://api.example.com/webhooks/digest"
        method:
          type: string
          enum: [GET, POST, PUT, PATCH, DELETE]
          default: GET
        headers:
          type: object
          additionalProperties:
            type: string
          example:
            Authorization: "Bearer secret"
            X-Source: "tickstem"
        body:
          type: string
          format: byte
          description: Base64-encoded request body (for POST/PUT/PATCH)
        timeout_secs:
          type: integer
          minimum: 1
          maximum: 300
          default: 30
        retries:
          type: integer
          minimum: 0
          maximum: 5
          default: 0
        status:
          type: string
          enum: [active, paused, failing]
          readOnly: true
        next_run_at:
          type: string
          format: date-time
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true

    JobCreate:
      type: object
      required: [name, schedule, endpoint]
      properties:
        name:
          type: string
          example: "Send digest email"
        description:
          type: string
        schedule:
          type: string
          description: Standard 5-field cron expression
          example: "0 9 * * 1-5"
        endpoint:
          type: string
          format: uri
          example: "https://api.example.com/webhooks/digest"
        method:
          type: string
          enum: [GET, POST, PUT, PATCH, DELETE]
          default: GET
        headers:
          type: object
          additionalProperties:
            type: string
        body:
          type: string
          format: byte
        timeout_secs:
          type: integer
          minimum: 1
          maximum: 300
          default: 30
        retries:
          type: integer
          minimum: 0
          maximum: 5
          default: 0

    Execution:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        job_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, running, success, failed, timeout]
        status_code:
          type: integer
          nullable: true
          example: 200
        duration_ms:
          type: integer
          nullable: true
          example: 342
        attempt_num:
          type: integer
          example: 1
        error:
          type: string
          description: Present when status is failed or timeout
        scheduled_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        retained_until:
          type: string
          format: date-time
          nullable: true
          description: When execution history is purged (based on plan)

    APIKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          example: "Production deploy key"
        prefix:
          type: string
          description: First 12 characters of the key for identification
          example: "tsk_aBcDeFgH"
        created_at:
          type: string
          format: date-time
          readOnly: true
        last_used_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true

    APIKeyCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: "Production deploy key"

    APIKeyCreated:
      allOf:
        - $ref: "#/components/schemas/APIKey"
        - type: object
          properties:
            key:
              type: string
              description: Full plaintext key — shown once, store securely
              example: "tsk_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdef"

    Error:
      type: object
      properties:
        error:
          type: string
          example: "invalid cron schedule"

    JobList:
      type: object
      properties:
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/Job"
        limit:
          type: integer
        offset:
          type: integer

    ExecutionList:
      type: object
      properties:
        executions:
          type: array
          items:
            $ref: "#/components/schemas/Execution"
        limit:
          type: integer
        offset:
          type: integer

    KeyList:
      type: object
      properties:
        keys:
          type: array
          items:
            $ref: "#/components/schemas/APIKey"

    Verification:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
          example: "user@example.com"
        valid:
          type: boolean
          description: Safe to store — passed all checks
        mx_found:
          type: boolean
          description: Domain has at least one MX record
        disposable:
          type: boolean
          description: Address belongs to a throwaway service
        role_based:
          type: boolean
          description: Generic inbox (admin@, info@, noreply@, ...)
        reason:
          type: string
          description: Explanation when valid is false
          example: "disposable email address"
        created_at:
          type: string
          format: date-time
          readOnly: true

    VerificationList:
      type: object
      properties:
        verifications:
          type: array
          items:
            $ref: "#/components/schemas/Verification"
        limit:
          type: integer
        offset:
          type: integer

    AssertionSource:
      type: string
      enum: [status_code, response_time, body]
      description: |
        Part of the HTTP response the assertion checks.
        `status_code` and `response_time` require an integer target.
        `body` requires a string target.

    AssertionComparison:
      type: string
      enum: [eq, ne, lt, lte, gt, gte, contains, not_contains]
      description: |
        Operator applied between the source value and target.
        `lt`, `lte`, `gt`, `gte` are only valid for numeric sources.
        `contains`, `not_contains` are only valid for `body`.

    Assertion:
      type: object
      required: [source, comparison, target]
      properties:
        source:
          $ref: "#/components/schemas/AssertionSource"
        comparison:
          $ref: "#/components/schemas/AssertionComparison"
        target:
          type: string
          description: Expected value. Integer string for numeric sources, plain string for body.
          example: "200"

    Monitor:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          example: "Production API"
        url:
          type: string
          format: uri
          example: "https://api.example.com/health"
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
          default: 60
          description: How often to check the endpoint
        timeout_secs:
          type: integer
          minimum: 5
          maximum: 30
          default: 10
        status:
          type: string
          enum: [active, paused, failing]
          readOnly: true
        ssl_expires_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
          description: SSL certificate expiry captured from the last HTTPS check
        assertions:
          type: array
          items:
            $ref: "#/components/schemas/Assertion"
          description: |
            When set, all assertions must pass for a check to be considered up.
            When empty, any 2xx or 3xx response is considered up.
        next_check_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true

    MonitorCreate:
      type: object
      required: [name, url]
      properties:
        name:
          type: string
          example: "Production API"
        url:
          type: string
          format: uri
          example: "https://api.example.com/health"
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
          default: 60
        timeout_secs:
          type: integer
          minimum: 5
          maximum: 30
          default: 10
        assertions:
          type: array
          items:
            $ref: "#/components/schemas/Assertion"

    MonitorUpdate:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
          format: uri
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
        timeout_secs:
          type: integer
          minimum: 5
          maximum: 30
        status:
          type: string
          enum: [active, paused]
          description: Use to pause or resume a monitor
        assertions:
          type: array
          items:
            $ref: "#/components/schemas/Assertion"

    MonitorList:
      type: object
      properties:
        monitors:
          type: array
          items:
            $ref: "#/components/schemas/Monitor"

    MonitorCheck:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        monitor_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [up, down, timeout]
        status_code:
          type: integer
          nullable: true
          example: 200
        duration_ms:
          type: integer
          example: 183
        error:
          type: string
          description: Present when status is down or timeout
        ssl_expires_at:
          type: string
          format: date-time
          nullable: true
          description: SSL certificate expiry captured during this check
        checked_at:
          type: string
          format: date-time

    MonitorCheckList:
      type: object
      properties:
        checks:
          type: array
          items:
            $ref: "#/components/schemas/MonitorCheck"
        limit:
          type: integer
        offset:
          type: integer

    Heartbeat:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          example: "daily backup"
        token:
          type: string
          description: 64-character hex token. Use as the credential when pinging.
          readOnly: true
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
          default: 3600
          description: Expected time between pings in seconds
        grace_secs:
          type: integer
          minimum: 0
          maximum: 86400
          default: 300
          description: Buffer after the deadline before an alert is sent
        status:
          type: string
          enum: [active, paused, failing]
          readOnly: true
        consecutive_misses:
          type: integer
          readOnly: true
          description: Number of consecutive missed pings
        last_pinged_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
        next_expected_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true

    HeartbeatCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          example: "daily backup"
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
          default: 3600
        grace_secs:
          type: integer
          minimum: 0
          maximum: 86400
          default: 300

    HeartbeatUpdate:
      type: object
      properties:
        name:
          type: string
        interval_secs:
          type: integer
          minimum: 60
          maximum: 86400
        grace_secs:
          type: integer
          minimum: 0
          maximum: 86400
        status:
          type: string
          enum: [active, paused]
          description: Set to paused to suppress alerts, active to resume.

    HeartbeatList:
      type: object
      properties:
        heartbeats:
          type: array
          items:
            $ref: "#/components/schemas/Heartbeat"

    HeartbeatPing:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        heartbeat_id:
          type: string
          format: uuid
        pinged_at:
          type: string
          format: date-time

    HeartbeatPingList:
      type: object
      properties:
        pings:
          type: array
          items:
            $ref: "#/components/schemas/HeartbeatPing"
        limit:
          type: integer

    HeartbeatPingResult:
      type: object
      properties:
        status:
          type: string
          enum: [ok, paused]

paths:
  /jobs:
    get:
      operationId: listJobs
      summary: List jobs
      tags: [Jobs]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    post:
      operationId: createJob
      summary: Create a job
      tags: [Jobs]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobCreate"
            examples:
              daily_digest:
                summary: Daily digest at 9am on weekdays
                value:
                  name: "Send digest email"
                  schedule: "0 9 * * 1-5"
                  endpoint: "https://api.example.com/webhooks/digest"
                  method: POST
              hourly_cleanup:
                summary: Hourly cleanup job
                value:
                  name: "Purge expired sessions"
                  schedule: "0 * * * *"
                  endpoint: "https://api.example.com/cron/cleanup"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "400":
          description: Validation error (invalid cron, missing fields)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized

  /jobs/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid

    get:
      operationId: getJob
      summary: Get a job
      tags: [Jobs]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "404":
          description: Not found

    put:
      operationId: updateJob
      summary: Update a job
      tags: [Jobs]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobCreate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "400":
          description: Validation error
        "404":
          description: Not found

    delete:
      operationId: deleteJob
      summary: Delete a job
      tags: [Jobs]
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found

  /jobs/{id}/pause:
    post:
      operationId: pauseJob
      summary: Pause a job
      tags: [Jobs]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK — job is now paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "404":
          description: Not found

  /jobs/{id}/resume:
    post:
      operationId: resumeJob
      summary: Resume a paused or failing job
      tags: [Jobs]
      description: Resumes a paused or failing job and resets the consecutive failure counter.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK — job is now active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "404":
          description: Not found

  /executions:
    get:
      operationId: listExecutions
      summary: List executions for a job
      tags: [Executions]
      parameters:
        - name: job_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecutionList"
        "400":
          description: Missing job_id

  /executions/{id}:
    get:
      operationId: getExecution
      summary: Get a single execution
      tags: [Executions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Execution"
        "404":
          description: Not found

  /keys:
    get:
      operationId: listKeys
      summary: List API keys
      tags: [Keys]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KeyList"

    post:
      operationId: createKey
      summary: Create an API key
      tags: [Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/APIKeyCreate"
      responses:
        "201":
          description: Created — key value shown once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIKeyCreated"
        "400":
          description: Missing name

  /keys/{id}:
    delete:
      operationId: revokeKey
      summary: Revoke an API key
      tags: [Keys]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Revoked
        "404":
          description: Not found

  /verify:
    post:
      operationId: verifyEmail
      summary: Verify an email address
      tags: [Verify]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: "user@example.com"
            examples:
              valid:
                summary: Valid business address
                value:
                  email: "alice@acmecorp.com"
              disposable:
                summary: Disposable address
                value:
                  email: "tmp@mailinator.com"
      responses:
        "200":
          description: OK — result regardless of whether the address is valid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Verification"
        "400":
          description: Missing or empty email field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: Monthly verification quota exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /verify/history:
    get:
      operationId: listVerifyHistory
      summary: List past verifications
      tags: [Verify]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificationList"
        "401":
          description: Unauthorized

  /monitors:
    get:
      operationId: listMonitors
      summary: List monitors
      tags: [Monitors]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonitorList"
        "401":
          description: Unauthorized

    post:
      operationId: createMonitor
      summary: Create a monitor
      tags: [Monitors]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonitorCreate"
            examples:
              simple:
                summary: Simple availability check
                value:
                  name: "Production API"
                  url: "https://api.example.com/health"
              with_assertions:
                summary: With response assertions
                value:
                  name: "API health check"
                  url: "https://api.example.com/health"
                  interval_secs: 60
                  assertions:
                    - source: status_code
                      comparison: eq
                      target: "200"
                    - source: response_time
                      comparison: lt
                      target: "2000"
                    - source: body
                      comparison: contains
                      target: '"status":"ok"'
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
        "402":
          description: Monitor quota reached for your plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /monitors/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid

    get:
      operationId: getMonitor
      summary: Get a monitor
      tags: [Monitors]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "404":
          description: Not found

    patch:
      operationId: updateMonitor
      summary: Update a monitor
      tags: [Monitors]
      description: |
        Only supplied fields are changed. To pause or resume a monitor set
        `status` to `paused` or `active`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonitorUpdate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "400":
          description: Validation error
        "402":
          description: interval_secs below minimum for your plan
        "404":
          description: Not found

    delete:
      operationId: deleteMonitor
      summary: Delete a monitor
      tags: [Monitors]
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found

  /monitors/{id}/pause:
    patch:
      operationId: pauseMonitor
      summary: Pause a monitor
      tags: [Monitors]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /monitors/{id}/resume:
    patch:
      operationId: resumeMonitor
      summary: Resume a paused monitor
      tags: [Monitors]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Monitor"
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /monitors/{id}/checks:
    get:
      operationId: listMonitorChecks
      summary: List check history for a monitor
      tags: [Monitors]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonitorCheckList"
        "404":
          description: Monitor not found

  /heartbeats:
    get:
      operationId: listHeartbeats
      summary: List heartbeats
      tags: [Heartbeats]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HeartbeatList"
        "401":
          description: Unauthorized
    post:
      operationId: createHeartbeat
      summary: Create a heartbeat
      tags: [Heartbeats]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HeartbeatCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Heartbeat"
        "400":
          description: Invalid parameters
        "401":
          description: Unauthorized
        "402":
          description: Heartbeat quota reached for your plan

  /heartbeats/{id}:
    get:
      operationId: getHeartbeat
      summary: Get a heartbeat
      tags: [Heartbeats]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Heartbeat"
        "401":
          description: Unauthorized
        "404":
          description: Heartbeat not found
    patch:
      operationId: updateHeartbeat
      summary: Update a heartbeat
      tags: [Heartbeats]
      description: |
        Only supplied fields are changed. To pause or resume a heartbeat set
        `status` to `paused` or `active`.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HeartbeatUpdate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Heartbeat"
        "400":
          description: Invalid parameters
        "401":
          description: Unauthorized
        "404":
          description: Heartbeat not found
    delete:
      operationId: deleteHeartbeat
      summary: Delete a heartbeat
      tags: [Heartbeats]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Deleted
        "401":
          description: Unauthorized
        "404":
          description: Heartbeat not found


  /heartbeats/{token}/ping:
    post:
      operationId: pingHeartbeat
      summary: Ping a heartbeat to record a successful run
      description: |
        No Authorization header required — the token is the credential.
        Returns `{"status":"ok"}` on success, or `{"status":"paused"}` if the heartbeat is paused.
      tags: [Heartbeats]
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
            description: 64-character hex ping token
      responses:
        "200":
          description: Ping recorded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HeartbeatPingResult"
        "404":
          description: Token not found

  /heartbeats/{id}/pings:
    get:
      operationId: listHeartbeatPings
      summary: List ping history for a heartbeat
      tags: [Heartbeats]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HeartbeatPingList"
        "401":
          description: Unauthorized
        "404":
          description: Heartbeat not found
