openapi: 3.0.3
info:
  title: Runhooks API
  description: |
    REST API for Runhooks — reliable HTTP scheduling infrastructure.
    Schedule, manage, and monitor HTTP webhooks programmatically.
  version: 1.0.0
  contact:
    email: support@runhooks.app
    url: https://runhooks.app
servers:
  - url: https://api.runhooks.app/api/v1
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Health
    description: System health check (public, no auth)
  - name: Account
    description: Registration, login, and account management
  - name: Jobs
    description: Create, read, update, and delete scheduled jobs
  - name: Executions
    description: View and replay job executions
  - name: Alerts
    description: Configure failure alerts (email, webhook, Slack)
  - name: Usage
    description: Plan usage and limits

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "API key (rh_live_...) or JWT access token"

  schemas:
    ApiResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
        message:
          type: string
        error:
          type: string

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string

    Schedule:
      type: object
      required: [type, expression]
      properties:
        type:
          type: string
          enum: [cron, interval]
        expression:
          type: string
          description: "Cron expression or interval in ms"
          example: "*/5 * * * *"
        timezone:
          type: string
          description: "IANA timezone (cron only)"
          example: "America/New_York"

    HttpConfig:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: "Target URL (no private IPs)"
        method:
          type: string
          enum: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS]
          default: GET
        headers:
          type: object
          additionalProperties:
            type: string
        body:
          type: string
          maxLength: 1048576
          description: "Request body (max 1 MB)"
        timeoutMs:
          type: integer
          minimum: 1000
          maximum: 300000
          default: 30000

    RetryPolicy:
      type: object
      properties:
        maxRetries:
          type: integer
          minimum: 0
          maximum: 10
          default: 3
        initialDelay:
          type: integer
          minimum: 100
          maximum: 60000
          default: 1000
          description: "Milliseconds"
        backoffMultiplier:
          type: number
          minimum: 1
          maximum: 10
          default: 2
        maxDelay:
          type: integer
          minimum: 1000
          maximum: 3600000
          default: 300000
          description: "Milliseconds"

    Job:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        schedule:
          $ref: '#/components/schemas/Schedule'
        httpConfig:
          $ref: '#/components/schemas/HttpConfig'
        retryPolicy:
          $ref: '#/components/schemas/RetryPolicy'
        status:
          type: string
          enum: [active, paused, completed, failed]
        tags:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        lastExecutionAt:
          type: string
          format: date-time
          nullable: true
        nextExecutionAt:
          type: string
          format: date-time
          nullable: true

    CreateJobRequest:
      type: object
      required: [name, schedule, httpConfig]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 2000
        schedule:
          $ref: '#/components/schemas/Schedule'
        httpConfig:
          $ref: '#/components/schemas/HttpConfig'
        retryPolicy:
          $ref: '#/components/schemas/RetryPolicy'
        tags:
          type: array
          items:
            type: string
            maxLength: 50
          maxItems: 20

    UpdateJobRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 2000
        schedule:
          $ref: '#/components/schemas/Schedule'
        httpConfig:
          $ref: '#/components/schemas/HttpConfig'
        retryPolicy:
          $ref: '#/components/schemas/RetryPolicy'
        status:
          type: string
          enum: [active, paused, completed, failed]
        tags:
          type: array
          items:
            type: string
            maxLength: 50
          maxItems: 20

    ExecutionLog:
      type: object
      properties:
        id:
          type: string
        jobId:
          type: string
        status:
          type: string
          enum: [pending, running, success, failed, timeout, dead_letter, quota_exceeded]
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          nullable: true
        durationMs:
          type: integer
          nullable: true
        httpStatusCode:
          type: integer
          nullable: true
        responseBody:
          type: string
          nullable: true
        errorMessage:
          type: string
          nullable: true
        attempt:
          type: integer
        retryOf:
          type: string
          nullable: true

    AlertConfig:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        name:
          type: string
        channel:
          type: string
          enum: [email, webhook, slack]
        target:
          type: string
        jobId:
          type: string
          nullable: true
        consecutiveFailuresThreshold:
          type: integer
        cooldownMinutes:
          type: integer
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateAlertRequest:
      type: object
      required: [name, channel, target]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        channel:
          type: string
          enum: [email, webhook, slack]
        target:
          type: string
          minLength: 1
          maxLength: 500
          description: "Email address, webhook URL, or Slack URL"
        jobId:
          type: string
          nullable: true
          description: "Scope to a specific job (omit for all jobs)"
        consecutiveFailuresThreshold:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
        cooldownMinutes:
          type: integer
          minimum: 0
          maximum: 10080
          default: 60
        enabled:
          type: boolean
          default: true

    UpdateAlertRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        channel:
          type: string
          enum: [email, webhook, slack]
        target:
          type: string
          minLength: 1
          maxLength: 500
        jobId:
          type: string
          nullable: true
        consecutiveFailuresThreshold:
          type: integer
          minimum: 1
          maximum: 100
        cooldownMinutes:
          type: integer
          minimum: 0
          maximum: 10080
        enabled:
          type: boolean

    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          nullable: true
        name:
          type: string
        role:
          type: string
          enum: [admin, user]
        plan:
          type: string
          enum: [free, starter, production, growth]
        isAnonymous:
          type: boolean
        apiKeyPrefix:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    AuthTokens:
      type: object
      properties:
        accessToken:
          type: string
        refreshToken:
          type: string

    UsageMetric:
      type: object
      properties:
        used:
          type: integer
        limit:
          type: integer
          description: "-1 when unlimited"
        unlimited:
          type: boolean
        resetsAt:
          type: string
          format: date-time
          description: "Only for rolling-window metrics"

    UsageSnapshot:
      type: object
      properties:
        plan:
          type: string
          enum: [free, starter, production, growth]
        limits:
          type: object
          properties:
            maxJobs:
              type: integer
            maxDailyRuns:
              type: integer
            maxMonthlyRuns:
              type: integer
            maxRetries:
              type: integer
            maxRequestTimeoutMs:
              type: integer
            maxAlertConfigs:
              type: integer
            retentionDays:
              type: integer
            concurrency:
              type: integer
        usage:
          type: object
          properties:
            jobs:
              $ref: '#/components/schemas/UsageMetric'
            dailyRuns:
              $ref: '#/components/schemas/UsageMetric'
            monthlyRuns:
              $ref: '#/components/schemas/UsageMetric'
            alertConfigs:
              $ref: '#/components/schemas/UsageMetric'

    HealthCheckResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded, down]
        version:
          type: string
        uptime:
          type: number
        timestamp:
          type: string
          format: date-time
        services:
          type: object
          additionalProperties:
            type: object
            properties:
              status:
                type: string
                enum: [ok, down]
              latencyMs:
                type: number

    PaginatedResponse:
      type: object
      properties:
        data:
          type: array
          items: {}
        total:
          type: integer
        page:
          type: integer
        limit:
          type: integer
        totalPages:
          type: integer

  parameters:
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    LimitParam:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

paths:
  /health:
    get:
      tags: [Health]
      summary: Health check
      description: Public endpoint — no authentication required.
      security: []
      responses:
        '200':
          description: System healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/HealthCheckResponse'
        '503':
          description: System degraded

  /auth/register:
    post:
      tags: [Account]
      summary: Register with email
      description: Create a full account with email and password. Returns JWT tokens and an API key.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, name]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                  maxLength: 128
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
      responses:
        '201':
          description: Registration successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/User'
                      tokens:
                        $ref: '#/components/schemas/AuthTokens'
                      apiKey:
                        type: string
        '409':
          description: Email already registered

  /auth/register-anonymous:
    post:
      tags: [Account]
      summary: Register anonymous
      description: Create an anonymous account — no email required. Returns an API key only.
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/User'
                      apiKey:
                        type: string

  /auth/login:
    post:
      tags: [Account]
      summary: Login
      description: Sign in with email and password. Returns JWT access and refresh tokens.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 1
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/User'
                      tokens:
                        $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Invalid email or password

  /auth/refresh:
    post:
      tags: [Account]
      summary: Refresh token
      description: Exchange a refresh token for new access and refresh tokens. Access tokens expire after 15 minutes; refresh tokens last 30 days.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        '200':
          description: Tokens refreshed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Invalid refresh token

  /auth/me:
    get:
      tags: [Account]
      summary: Get current user
      description: Get the authenticated user's profile.
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/User'

  /auth/rotate-key:
    post:
      tags: [Account]
      summary: Rotate API key
      description: Generate a new API key. The old key stops working immediately.
      responses:
        '200':
          description: Key rotated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      apiKey:
                        type: string
                      apiKeyPrefix:
                        type: string

  /jobs:
    post:
      tags: [Jobs]
      summary: Create job
      description: Create a new scheduled job.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
      responses:
        '201':
          description: Job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Job'
        '400':
          description: Validation error
        '403':
          description: Plan limit reached
    get:
      tags: [Jobs]
      summary: List jobs
      description: List your jobs with optional filters and pagination.
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: status
          in: query
          schema:
            type: string
            enum: [active, paused, completed, failed]
        - name: tag
          in: query
          schema:
            type: string
        - name: name
          in: query
          schema:
            type: string
            maxLength: 100
          description: Case-insensitive regex filter
      responses:
        '200':
          description: Paginated job list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    allOf:
                      - $ref: '#/components/schemas/PaginatedResponse'
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: '#/components/schemas/Job'

  /jobs/{id}:
    get:
      tags: [Jobs]
      summary: Get job
      description: Get a single job by ID.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/Job'
        '404':
          description: Job not found
    patch:
      tags: [Jobs]
      summary: Update job
      description: Update a job. Only include the fields you want to change.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateJobRequest'
      responses:
        '200':
          description: Job updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Job'
        '404':
          description: Job not found
    put:
      tags: [Jobs]
      summary: Update job (PUT)
      description: Same as PATCH — update a job with partial data.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateJobRequest'
      responses:
        '200':
          description: Job updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Job'
        '404':
          description: Job not found
    delete:
      tags: [Jobs]
      summary: Delete job
      description: Delete a job permanently.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
        '404':
          description: Job not found

  /jobs/{id}/executions:
    get:
      tags: [Jobs]
      summary: Get job executions
      description: List executions for a specific job.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, running, success, failed, timeout, dead_letter, quota_exceeded]
      responses:
        '200':
          description: Paginated execution list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    allOf:
                      - $ref: '#/components/schemas/PaginatedResponse'
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: '#/components/schemas/ExecutionLog'
        '404':
          description: Job not found

  /executions:
    get:
      tags: [Executions]
      summary: List executions
      description: List executions across all your jobs.
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/LimitParam'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, running, success, failed, timeout, dead_letter, quota_exceeded]
        - name: jobId
          in: query
          schema:
            type: string
          description: Filter by job ID (must be a job you own)
      responses:
        '200':
          description: Paginated execution list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    allOf:
                      - $ref: '#/components/schemas/PaginatedResponse'
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: '#/components/schemas/ExecutionLog'

  /executions/{id}/replay:
    post:
      tags: [Executions]
      summary: Replay execution
      description: Re-run a past execution immediately. Creates a new execution linked to the original.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '202':
          description: Replay enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Execution replay enqueued"
        '404':
          description: Execution or job not found

  /alerts:
    post:
      tags: [Alerts]
      summary: Create alert
      description: Create a new alert configuration.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAlertRequest'
      responses:
        '201':
          description: Alert created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/AlertConfig'
        '400':
          description: Validation error
        '403':
          description: Alert config limit reached
    get:
      tags: [Alerts]
      summary: List alerts
      description: List all your alert configurations. Returns a flat array.
      responses:
        '200':
          description: Alert list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AlertConfig'

  /alerts/{id}:
    get:
      tags: [Alerts]
      summary: Get alert
      description: Get a single alert configuration by ID.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Alert details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/AlertConfig'
        '404':
          description: Alert config not found
    put:
      tags: [Alerts]
      summary: Update alert
      description: Update an alert configuration. Only include the fields you want to change.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAlertRequest'
      responses:
        '200':
          description: Alert updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/AlertConfig'
        '404':
          description: Alert config not found
    delete:
      tags: [Alerts]
      summary: Delete alert
      description: Delete an alert configuration.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Alert deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
        '404':
          description: Alert config not found

  /usage/me:
    get:
      tags: [Usage]
      summary: Get usage
      description: Get your current plan, limits, and usage.
      responses:
        '200':
          description: Usage snapshot
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/UsageSnapshot'
