openapi: 3.1.0
info:
  title: CronAlive API
  version: '1.0'
  description: |
    Public REST API v1. Authenticate with a project API key
    (`Authorization: Bearer ca_rw_…` or `ca_ro_…`).
    Read-only keys may only call GET endpoints.
    Rate limit: 600 requests/minute per key (429 + Retry-After on excess).
servers:
  - url: https://app.cronalive.com/api/v1
security:
  - bearerAuth: []

paths:
  /ping-domains:
    get:
      summary: Current ping domains (fallback list for SDKs)
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { type: string, format: uri }
                    example: ['https://ping.cronalive.com', 'https://ping.cronalive.ru']

  /checks:
    get:
      summary: List checks of the key's project
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Check' }
    post:
      summary: Create a check (rw key)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CheckInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Check' }
              example:
                data:
                  id: e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301
                  kind: heartbeat
                  name: nightly backup
                  slug: nightly-backup
                  description: null
                  tags: [backups, prod]
                  schedule: { kind: cron, cron: '0 3 * * *', tz: Europe/Moscow }
                  grace_sec: 1800
                  tz: Europe/Moscow
                  status: new
                  last_ping_at: null
                  last_duration_ms: null
                  next_deadline_at: null
                  ping_urls:
                    - 'https://ping.cronalive.com/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
                    - 'https://ping.cronalive.ru/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
                  created_at: '2026-07-22T10:00:00+00:00'
        '422': { $ref: '#/components/responses/ValidationError' }

  /checks/{uuid}:
    parameters:
      - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      summary: Get a check
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Check' }
        '404': { description: Not found }
    patch:
      summary: Update a check (rw key)
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CheckInput' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Check' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      summary: Delete a check (rw key)
      responses:
        '204': { description: Deleted }

  /checks/{uuid}/pause:
    post:
      summary: Pause monitoring (rw key)
      description: >
        The check flips to `paused`: pings are still recorded, but the status
        and deadlines are not touched until `resume`.
      parameters:
        - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Check' }
              example:
                data:
                  id: e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301
                  kind: heartbeat
                  name: nightly backup
                  slug: nightly-backup
                  description: null
                  tags: [backups, prod]
                  schedule: { kind: cron, cron: '0 3 * * *', tz: Europe/Moscow }
                  grace_sec: 1800
                  tz: Europe/Moscow
                  status: paused
                  last_ping_at: '2026-07-22T03:00:12+00:00'
                  last_duration_ms: 5230
                  next_deadline_at: null
                  ping_urls:
                    - 'https://ping.cronalive.com/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
                    - 'https://ping.cronalive.ru/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
                  created_at: '2026-07-01T10:00:00+00:00'

  /checks/{uuid}/resume:
    post:
      summary: Resume monitoring (rw key)
      description: >
        Returns to `up` with a fresh deadline counted from the resume moment
        (or to `new` if the check has never been pinged).
      parameters:
        - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          description: Resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Check' }

  /checks/{uuid}/pings:
    get:
      summary: Recent pings (up to 100)
      parameters:
        - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Ping' }

  /checks/{uuid}/flips:
    get:
      summary: Recent status changes
      parameters:
        - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Flip' }

  /integrations:
    get:
      summary: List integrations
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Integration' }
    post:
      summary: Create an integration (rw key)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IntegrationInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Integration' }
                  telegram_deep_link:
                    type: string
                    description: Only for kind=telegram — open it to link a chat
                    example: 'https://t.me/CronAliveBot?start=Zx91…'
        '422': { $ref: '#/components/responses/ValidationError' }

  /integrations/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    patch:
      summary: Update an integration (rw key)
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IntegrationInput' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Integration' }
    delete:
      summary: Delete an integration (rw key)
      responses:
        '204': { description: Deleted }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer }

  responses:
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            type: object
            properties:
              message: { type: string, example: 'The name field is required.' }
              errors:
                type: object
                example: { name: ['The name field is required.'] }

  schemas:
    Schedule:
      type: object
      description: period | cron | oncalendar (systemd)
      oneOf:
        - properties:
            kind: { const: period }
            period_sec: { type: integer, minimum: 60, maximum: 31536000, example: 3600 }
            tz: { type: string, example: UTC }
        - properties:
            kind: { const: cron }
            cron: { type: string, description: 5-field cron expression, example: '0 3 * * *' }
            tz: { type: string, example: Europe/Moscow }
        - properties:
            kind: { const: oncalendar }
            oncalendar: { type: string, description: systemd OnCalendar expression, example: 'Mon..Fri 09:00' }
            tz: { type: string, example: UTC }
      example: { kind: cron, cron: '0 3 * * *', tz: Europe/Moscow }

    CheckInput:
      type: object
      required: [name, grace_sec, schedule]
      example:
        name: nightly backup
        kind: heartbeat
        slug: nightly-backup
        tags: [backups, prod]
        tz: Europe/Moscow
        grace_sec: 1800
        schedule: { kind: cron, cron: '0 3 * * *' }
      properties:
        name: { type: string, maxLength: 255, example: nightly backup }
        kind: { type: string, enum: [heartbeat, http], default: heartbeat }
        slug: { type: string, pattern: '^[a-z0-9][a-z0-9_-]{0,99}$', example: nightly-backup }
        description: { type: string }
        tags: { type: array, items: { type: string }, example: [backups, prod] }
        tz: { type: string, default: UTC }
        grace_sec: { type: integer, minimum: 0, maximum: 2592000, example: 1800 }
        schedule: { $ref: '#/components/schemas/Schedule' }
        http_config:
          type: object
          description: Required when kind=http
          example:
            url: 'https://example.com/health'
            method: GET
            expected_codes: ['2xx', '3xx']
            keyword: OK
            ssl_days_before: 14
            confirmations: 2
          properties:
            url: { type: string, format: uri, example: 'https://example.com/health' }
            method: { type: string, enum: [GET, HEAD, POST], default: GET }
            timeout_sec: { type: integer, minimum: 1, maximum: 30, default: 10 }
            expected_codes: { type: array, items: { type: string }, default: ['2xx', '3xx'] }
            keyword: { type: string, example: OK }
            keyword_absent: { type: boolean, default: false }
            follow_redirects: { type: boolean, default: true }
            headers: { type: object, additionalProperties: { type: string } }
            basic_auth:
              type: object
              properties:
                username: { type: string }
                password: { type: string }
            ssl_days_before: { type: integer, enum: [7, 14, 30], default: 14 }
            confirmations: { type: integer, minimum: 1, maximum: 5, default: 2 }

    Check:
      type: object
      example:
        id: e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301
        kind: heartbeat
        name: nightly backup
        slug: nightly-backup
        description: null
        tags: [backups, prod]
        schedule: { kind: cron, cron: '0 3 * * *', tz: Europe/Moscow }
        grace_sec: 1800
        tz: Europe/Moscow
        status: up
        last_ping_at: '2026-07-22T03:00:12+00:00'
        last_duration_ms: 5230
        next_deadline_at: '2026-07-23T00:00:00+00:00'
        ping_urls:
          - 'https://ping.cronalive.com/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
          - 'https://ping.cronalive.ru/e4c9ffb6-9d7e-4c1f-9df6-04642b6ad301'
        created_at: '2026-07-01T10:00:00+00:00'
      properties:
        id: { type: string, format: uuid }
        kind: { type: string, enum: [heartbeat, http] }
        name: { type: string }
        slug: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        tags: { type: array, items: { type: string } }
        schedule: { $ref: '#/components/schemas/Schedule' }
        grace_sec: { type: integer }
        tz: { type: string }
        status: { type: string, enum: [new, up, late, down, paused] }
        last_ping_at: { type: [string, 'null'], format: date-time }
        last_duration_ms: { type: [integer, 'null'] }
        next_deadline_at: { type: [string, 'null'], format: date-time }
        ping_urls: { type: array, items: { type: string, format: uri } }
        created_at: { type: string, format: date-time }

    Ping:
      type: object
      example:
        ts: '2026-07-22T03:00:12.482913+00:00'
        type: success
        exit_status: 0
        source_ip: 203.0.113.7
        method: GET
        body_size: 0
        duration_ms: 5230
        body: null
        user_agent: curl/8.5.0
      properties:
        ts: { type: string, format: date-time }
        type: { type: string, enum: [success, start, fail, exit, log] }
        exit_status: { type: [integer, 'null'] }
        source_ip: { type: [string, 'null'] }
        method: { type: string }
        body_size: { type: integer }
        duration_ms: { type: [integer, 'null'] }
        body: { type: [string, 'null'], description: Decrypted POST body (truncated to 2 KB) }
        user_agent: { type: [string, 'null'] }

    Flip:
      type: object
      example:
        ts: '2026-07-21T00:30:00+00:00'
        old_status: late
        new_status: down
        reason: timeout
      properties:
        ts: { type: string, format: date-time }
        old_status: { type: string, enum: [new, up, late, down, paused] }
        new_status: { type: string, enum: [new, up, late, down, paused] }
        reason: { type: string, enum: [timeout, fail, exit, ping, manual, resumed, quota] }

    IntegrationInput:
      type: object
      required: [kind, name, events]
      example:
        kind: telegram
        name: ops chat
        events: [down, up]
        tags: [prod]
        quiet_hours: { from: '23:00', to: '08:00', tz: Europe/Moscow }
        reminders: [1, 24]
      properties:
        kind: { type: string, enum: [telegram, email, webhook, slack, discord, mattermost, sms, pagerduty] }
        name: { type: string, maxLength: 255 }
        events:
          type: array
          items: { type: string, enum: [down, up, late] }
          example: [down, up]
        tags: { type: array, items: { type: string }, description: 'Only checks with these tags; null = all' }
        enabled: { type: boolean, default: true }
        quiet_hours:
          type: object
          properties:
            from: { type: string, example: '23:00' }
            to: { type: string, example: '08:00' }
            tz: { type: string, example: Europe/Moscow }
        reminders:
          type: array
          items: { type: integer, minimum: 1, maximum: 168 }
          description: Repeat reminders while down, hours
          example: [1, 24]
        config:
          type: object
          description: |
            Channel-specific (not required for telegram — it is linked via
            the deep link): email → {email},
            webhook → {url, secret?, body_template?, content_type?}
            (body_template supports {{event}}, {{check_id}}, {{check_name}},
            {{status}}, {{previous_status}}, {{reason}}, {{ts}}, {{tags}},
            {{subject}}, {{message}}; values are JSON-escaped when
            content_type is application/json),
            slack/discord/mattermost → {webhook_url}, sms → {phone},
            pagerduty → {routing_key}
          example: { email: 'ops@example.com' }

    Integration:
      type: object
      description: Channel secrets (config) are never returned by the API
      example:
        id: 7
        kind: telegram
        name: ops chat
        enabled: true
        events: [down, up]
        tags: [prod]
        quiet_hours: { from: '23:00', to: '08:00', tz: Europe/Moscow }
        reminders: [1, 24]
        created_at: '2026-07-10T09:00:00+00:00'
      properties:
        id: { type: integer }
        kind: { type: string, enum: [telegram, email, webhook, slack, discord, mattermost, sms, pagerduty] }
        name: { type: string }
        enabled: { type: boolean }
        events: { type: array, items: { type: string } }
        tags: { type: [array, 'null'], items: { type: string } }
        quiet_hours: { type: [object, 'null'] }
        reminders: { type: [array, 'null'], items: { type: integer } }
        created_at: { type: string, format: date-time }
