openapi: 3.1.0
info:
  title: Celper AI API
  version: "1.0"
  description: |
    Programmatic access to the Celper AI interview platform.
    Import candidates, fetch analysis results, trigger invitations,
    and receive webhook notifications.
  contact:
    name: Celper AI Support
    url: https://celper.ai
  license:
    name: Proprietary

servers:
  - url: https://api.celper.ai/v1
    description: Production

security:
  - apiKey: []

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key with format `celp_{32 hex chars}`"

  schemas:
    Error:
      type: object
      required: [success, error, message, requestId]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
          example: VALIDATION_ERROR
        message:
          type: string
          example: "Invalid email format"
        requestId:
          type: string
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

    Pagination:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
          description: Opaque cursor for next page
        hasMore:
          type: boolean

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Insufficient scope, role, or access
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ValidationError:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    JobPositionSummary:
      type: object
      properties:
        id:
          type: string
        position:
          type: string
        department:
          type: string
        location:
          type: string
        type:
          type: string
        candidateCount:
          type: integer
        createdAt:
          type: string
          format: date-time

    JobPositionDetail:
      type: object
      properties:
        id:
          type: string
        position:
          type: string
        department:
          type: string
        location:
          type: string
        type:
          type: string
        salary:
          type: number
          nullable: true
        currency:
          type: string
          nullable: true
        privacy:
          type: string
          enum: [public, private]
        description:
          type: string
        experience:
          type: string
        remote:
          type: boolean
        requirements:
          type: array
          items:
            type: string
        benefits:
          type: array
          items:
            type: string
        conversationVersion:
          type: string
        interviewLanguages:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time

    CandidateSummary:
      type: object
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
          nullable: true
        status:
          $ref: "#/components/schemas/CandidateStatus"
        cvScore:
          type: number
          nullable: true
        interviewScore:
          type: number
          nullable: true
        analysisViewed:
          type: boolean
        importSource:
          type: string
          enum: [api, csv, manual, cv_upload, email]
        createdAt:
          type: string
          format: date-time

    CandidateDetail:
      type: object
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
          nullable: true
        status:
          $ref: "#/components/schemas/CandidateStatus"
        cvScore:
          type: number
          nullable: true
        interviewScore:
          type: number
          nullable: true
        notes:
          type: string
          nullable: true
        cvText:
          type: string
          nullable: true
        aiEvaluation:
          type: string
          nullable: true
        upsides:
          type: array
          items:
            type: string
        shortcomings:
          type: array
          items:
            type: string
        analysisViewed:
          type: boolean
        importSource:
          type: string
        importedWithoutCv:
          type: boolean
        originalCvFileName:
          type: string
          nullable: true
        sessionId:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CandidateStatus:
      type: string
      enum: [new, invited, interviewed, selected, rejected, expired, failed, unevaluable]

    CandidateImportRequest:
      type: object
      required: [jobPositionId, firstName, lastName, email]
      properties:
        jobPositionId:
          type: string
        firstName:
          type: string
          minLength: 1
          maxLength: 100
        lastName:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
        phone:
          type: string
          maxLength: 50
        cvText:
          type: string
          description: Max 100KB
        notes:
          type: string
          description: Max 10KB
        skipAnalysis:
          type: boolean
          default: false

    BulkImportRequest:
      type: object
      required: [jobPositionId, candidates]
      properties:
        jobPositionId:
          type: string
        candidates:
          type: array
          minItems: 1
          maxItems: 500
          items:
            type: object
            required: [firstName, lastName, email]
            properties:
              firstName:
                type: string
                minLength: 1
                maxLength: 100
              lastName:
                type: string
                minLength: 1
                maxLength: 100
              email:
                type: string
                format: email
              phone:
                type: string
                maxLength: 50
              cvText:
                type: string
        skipAnalysis:
          type: boolean
          default: false

    WebhookSummary:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEvent"
        statusFilters:
          type: array
          items:
            $ref: "#/components/schemas/CandidateStatus"
        isActive:
          type: boolean
        failureCount:
          type: integer
        lastDeliveryAt:
          type: string
          format: date-time
          nullable: true
        lastDeliveryStatus:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    WebhookEvent:
      type: string
      enum: [analysis_ready, candidate_status_changed, cv_analysis_complete, bulk_import_complete]

    WebhookRegisterRequest:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: HTTPS URL. Private IPs and localhost are blocked.
        events:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/WebhookEvent"
        statusFilters:
          type: array
          items:
            $ref: "#/components/schemas/CandidateStatus"

    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
        webhookId:
          type: string
        event:
          type: string
        status:
          type: string
          enum: [delivered, failed, exhausted]
        attemptCount:
          type: integer
        httpStatus:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        lastAttemptAt:
          type: string
          format: date-time

  parameters:
    jobPositionId:
      name: jobPositionId
      in: query
      required: true
      schema:
        type: string
    limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    cursor:
      name: cursor
      in: query
      schema:
        type: string
    idempotencyKey:
      name: Idempotency-Key
      in: header
      schema:
        type: string
      description: Unique key to prevent duplicate operations (24h TTL)

paths:
  /health:
    get:
      summary: Health check
      operationId: getHealth
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: v1
                  timestamp:
                    type: string
                    format: date-time

  /job-positions:
    get:
      summary: List job positions
      operationId: listJobPositions
      tags: [Job Positions]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - name: privacy
          in: query
          schema:
            type: string
            enum: [public, private]
      responses:
        "200":
          description: Paginated list of job positions
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/JobPositionSummary"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /job-positions/{jobPositionId}:
    get:
      summary: Get job position details
      operationId: getJobPosition
      tags: [Job Positions]
      parameters:
        - name: jobPositionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job position details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    $ref: "#/components/schemas/JobPositionDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates:
    get:
      summary: List candidates
      operationId: listCandidates
      tags: [Candidates]
      parameters:
        - $ref: "#/components/parameters/jobPositionId"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/CandidateStatus"
        - name: importSource
          in: query
          schema:
            type: string
            enum: [api, csv, manual, cv_upload, email]
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: Paginated list of candidates
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CandidateSummary"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      summary: Import a single candidate
      operationId: importCandidate
      tags: [Candidates]
      parameters:
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CandidateImportRequest"
      responses:
        "201":
          description: Candidate created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      candidateId:
                        type: string
                      status:
                        type: string
                      cvScore:
                        type: number
                        nullable: true
                      firstName:
                        type: string
                      lastName:
                        type: string
                      email:
                        type: string
                      aiEvaluation:
                        type: string
                        nullable: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "409":
          description: Duplicate candidate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/bulk:
    post:
      summary: Bulk import candidates
      operationId: bulkImportCandidates
      tags: [Candidates]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkImportRequest"
      responses:
        "200":
          description: Synchronous result (skipAnalysis + ≤50 candidates)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      imported:
                        type: integer
                      skipped:
                        type: integer
                      failed:
                        type: integer
                      results:
                        type: array
                        items:
                          type: object
        "202":
          description: Async processing started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      bulkImportId:
                        type: string
                      status:
                        type: string
                        example: processing
                      totalCandidates:
                        type: integer
                      message:
                        type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}:
    get:
      summary: Get candidate details
      operationId: getCandidate
      tags: [Candidates]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
      responses:
        "200":
          description: Candidate details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    $ref: "#/components/schemas/CandidateDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      summary: Update candidate
      operationId: updateCandidate
      tags: [Candidates]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                firstName:
                  type: string
                lastName:
                  type: string
                email:
                  type: string
                  format: email
                phone:
                  type: string
                notes:
                  type: string
      responses:
        "200":
          description: Updated candidate
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    $ref: "#/components/schemas/CandidateDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Duplicate candidate
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      summary: Delete candidate
      operationId: deleteCandidate
      tags: [Candidates]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
        - name: force
          in: query
          schema:
            type: string
            enum: ["true"]
          description: Skip session check for invited candidates
      responses:
        "200":
          description: Candidate deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      deleted:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Conflict — candidate has active session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/cv-score:
    get:
      summary: Get CV score
      operationId: getCvScore
      tags: [Candidate Analysis]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
      responses:
        "200":
          description: CV score
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      cvScore:
                        type: number
                        nullable: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/cv-analysis:
    get:
      summary: Get CV analysis
      operationId: getCvAnalysis
      tags: [Candidate Analysis]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
        - name: language
          in: query
          schema:
            type: string
            enum: [en, lt]
      responses:
        "200":
          description: CV analysis with AI evaluation
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      cvScore:
                        type: number
                      aiEvaluation:
                        type: string
                      notes:
                        type: string
                      upsides:
                        type: array
                        items:
                          type: string
                      shortcomings:
                        type: array
                        items:
                          type: string
                      language:
                        type: string
                      isTranslated:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/cv-document:
    get:
      summary: Get CV document download URL
      operationId: getCvDocument
      tags: [Candidate Analysis]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
      responses:
        "200":
          description: CV document info with signed download URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      available:
                        type: boolean
                      downloadUrl:
                        type: string
                        format: uri
                      fileName:
                        type: string
                      contentType:
                        type: string
                      expiresAt:
                        type: string
                        format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/interview-analysis:
    get:
      summary: Get interview analysis
      operationId: getInterviewAnalysis
      tags: [Candidate Analysis]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
        - name: language
          in: query
          schema:
            type: string
            enum: [en, lt]
        - name: If-None-Match
          in: header
          schema:
            type: string
          description: ETag from previous response for conditional requests
      responses:
        "200":
          description: Interview analysis
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      interviewScore:
                        type: number
                      analysisVersion:
                        type: string
                      analysis:
                        type: object
                      analysisCompletedAt:
                        type: string
                        format: date-time
                      language:
                        type: string
                      isTranslated:
                        type: boolean
                      originalLanguage:
                        type: string
        "304":
          description: Not modified (ETag match)
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/status:
    get:
      summary: Get candidate status
      operationId: getCandidateStatus
      tags: [Candidate Actions]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/jobPositionId"
      responses:
        "200":
          description: Candidate status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      status:
                        $ref: "#/components/schemas/CandidateStatus"
                      analysisViewed:
                        type: boolean
                      interviewScore:
                        type: number
                        nullable: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/invite:
    post:
      summary: Send interview invitation
      operationId: inviteCandidate
      tags: [Candidate Actions]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobPositionId]
              properties:
                jobPositionId:
                  type: string
                emailSubject:
                  type: string
                  maxLength: 200
                emailBody:
                  type: string
                  maxLength: 5000
                  description: "Use {interviewLink} placeholder for the interview URL"
      responses:
        "200":
          description: Invitation sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      invited:
                        type: boolean
                      sessionId:
                        type: string
                      emailDelivery:
                        type: string
                        example: pending
                      expiresAt:
                        type: string
                        format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/select:
    post:
      summary: Select candidate
      operationId: selectCandidate
      tags: [Candidate Actions]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobPositionId]
              properties:
                jobPositionId:
                  type: string
      responses:
        "200":
          description: Candidate selected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: selected
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Invalid status transition or analysis not viewed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /candidates/{candidateId}/reject:
    post:
      summary: Reject candidate
      operationId: rejectCandidate
      tags: [Candidate Actions]
      parameters:
        - name: candidateId
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/idempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobPositionId]
              properties:
                jobPositionId:
                  type: string
      responses:
        "200":
          description: Candidate rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: rejected
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks:
    get:
      summary: List webhooks
      operationId: listWebhooks
      tags: [Webhooks]
      responses:
        "200":
          description: List of registered webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      summary: Register webhook
      operationId: registerWebhook
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookRegisterRequest"
      responses:
        "201":
          description: Webhook registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      webhookId:
                        type: string
                      secret:
                        type: string
                        description: HMAC-SHA256 signing secret
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "409":
          description: Webhook limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}:
    get:
      summary: Get webhook details
      operationId: getWebhook
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook details (secret excluded)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    $ref: "#/components/schemas/WebhookSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      summary: Update webhook
      operationId: updateWebhook
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/WebhookEvent"
                statusFilters:
                  type: array
                  items:
                    $ref: "#/components/schemas/CandidateStatus"
                isActive:
                  type: boolean
      responses:
        "200":
          description: Updated webhook
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    $ref: "#/components/schemas/WebhookSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      summary: Delete webhook
      operationId: deleteWebhook
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      deleted:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}/test:
    post:
      summary: Send test webhook
      operationId: testWebhook
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Test delivery enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      tested:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}/rotate-secret:
    post:
      summary: Rotate webhook secret
      operationId: rotateWebhookSecret
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: New secret generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: object
                    properties:
                      webhookId:
                        type: string
                      secret:
                        type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}/deliveries:
    get:
      summary: List webhook deliveries
      operationId: listWebhookDeliveries
      tags: [Webhooks]
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 20
        - $ref: "#/components/parameters/cursor"
        - name: status
          in: query
          schema:
            type: string
            enum: [delivered, failed, exhausted]
      responses:
        "200":
          description: Paginated delivery history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  requestId:
                    type: string
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
