openapi: 3.1.0
info:
  title: HireInterviewAI Integrations API
  version: "1.0.0"
  description: |
    Programmatic API for scheduling AI technical interviews and pulling scored
    reports from other platforms (ATS, Slack, Zapier, Merge.dev, custom backends).

    See `docs/INTEGRATIONS_PLATFORM_PLAN.md` for the strategy. This spec is the
    public contract — it is deliberately narrower and more stable than the
    first-party dashboard API.

    ## Authentication
    Every request authenticates with an **org API key** (created in the dashboard
    under **Settings → Developers**). Send it either way:

      - `Authorization: Bearer hia_xxx`
      - `X-API-Key: hia_xxx`

    Keys carry **scopes** — an endpoint returns `403` if the key lacks the scope
    it needs (`interviews:write`, `interviews:read`, `reports:read`). The secret
    is shown once at creation and stored only as a hash; rotate by creating a new
    key and revoking the old one.

    ## Response envelope
    Every response is wrapped:
    ```json
    { "success": true,  "data": { ... }, "error": null }
    { "success": false, "data": null,    "error": { "code": "...", "message": "..." } }
    ```
    Schemas below describe the `data` payload.

    ## Idempotency
    `POST` requests accept an optional `Idempotency-Key` header. Retrying with the
    same key replays the original response (header `Idempotent-Replayed: true`) and
    never creates — or charges for — a second interview. Reusing a key with a
    different body returns `409`.

servers:
  - url: https://api.hireinterviewai.com
    description: Production

security:
  - BearerKey: []
  - HeaderKey: []

tags:
  - name: Positions
  - name: Interviews
  - name: Reports

paths:
  /api/v1/integrations/positions:
    get:
      tags: [Positions]
      summary: List schedulable positions
      description: |
        Lists the org's **open** (schedulable) Positions so you can resolve a
        `position_id` to schedule against. A Position is built in the dashboard and
        holds the full interview configuration; this endpoint deliberately returns
        only `id`, `title`, `seniority`, and `status` — never the underlying topics,
        focus areas, or interview types.

        Requires scope `interviews:read`.
      operationId: listPositions
      security:
        - BearerKey: []
        - HeaderKey: []
      responses:
        '200':
          description: Positions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionListEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/integrations/interviews:
    post:
      tags: [Interviews]
      summary: Schedule an interview
      description: |
        Schedules an interview from a **Position** and returns the candidate invite
        link. The Position (built in the dashboard) supplies the entire interview
        configuration — topics, focus areas, interview types, difficulty, seniority,
        and duration — so the request body is just the position reference plus the
        candidate. The candidate joins at `invite_url`; there is no fixed start time.

        The Position must be **open** (see `GET /positions`). Billing (wallet debit)
        happens here; an insufficient balance returns `402`.

        Requires scope `interviews:write`.
      operationId: createInterview
      security:
        - BearerKey: []
        - HeaderKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleRequest'
      responses:
        '201':
          description: Interview created
          headers:
            Idempotent-Replayed:
              description: Present and `true` when this is a replayed idempotent response.
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '422': { $ref: '#/components/responses/ValidationFailed' }

  /api/v1/integrations/interviews/{id}:
    get:
      tags: [Interviews]
      summary: Get interview status
      description: Requires scope `interviews:read`.
      operationId: getInterview
      security:
        - BearerKey: []
        - HeaderKey: []
      parameters:
        - $ref: '#/components/parameters/InterviewID'
      responses:
        '200':
          description: Interview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/integrations/interviews/{id}/report:
    get:
      tags: [Reports]
      summary: Get interview report
      description: |
        Returns the scored report once the interview is complete. Poll this, or
        subscribe to the `report.generated` webhook to be pushed the result.

        Requires scope `reports:read`. Returns `404` until a report exists.
      operationId: getReport
      security:
        - BearerKey: []
        - HeaderKey: []
      parameters:
        - $ref: '#/components/parameters/InterviewID'
      responses:
        '200':
          description: Report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

webhooks:
  report.generated:
    post:
      summary: Interview scored (report ready)
      description: |
        Delivered to your configured webhook endpoint(s) once an interview is
        scored and its report is persisted. Use it to sync the outcome without
        polling. Other lifecycle events (`interview.scheduled`, `interview.started`,
        `interview.completed`, `interview.canceled`, `proctoring.alert`) are
        available the same way.

        ## Signature
        Every delivery is signed. Verify before trusting:
        `X-Signature-256: sha256=HMAC_SHA256(secret, "{timestamp}.{body}")`
        where `{timestamp}` is the `X-Webhook-Timestamp` header. Deliveries retry
        up to 3 times (2s / 4s / 8s backoff) on non-2xx.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledge receipt (any 2xx stops retries).

components:
  securitySchemes:
    BearerKey:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer hia_xxx`"
    HeaderKey:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    InterviewID:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Opaque client-chosen key to make a retried create safe.
      schema: { type: string, maxLength: 255 }

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Forbidden:
      description: API key lacks the required scope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    PaymentRequired:
      description: Wallet balance can't cover the interview
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: Resource not found (or not in your org)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    IdempotencyConflict:
      description: Idempotency-Key reused with a different body, or a same-key request is still in flight
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    ValidationFailed:
      description: Request body failed validation
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:
    Candidate:
      type: object
      required: [email, full_name]
      properties:
        email: { type: string, format: email, maxLength: 254 }
        full_name: { type: string, minLength: 1, maxLength: 200 }
        phone: { type: string, maxLength: 30 }
        resume_url: { type: string, format: uri, maxLength: 2000 }

    Seniority:
      type: string
      enum: [junior, mid, senior, staff, lead, principal]

    Recommendation:
      type: string
      enum: [strong_hire, hire, discuss, no_hire]

    ScheduleRequest:
      type: object
      required: [position_id, candidate]
      properties:
        position_id:
          type: string
          format: uuid
          description: The Position to schedule against — must be open. See `GET /positions`. Supplies all interview config.
        candidate: { $ref: '#/components/schemas/Candidate' }
        scheduled_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        round_number:
          type: integer
          description: Multi-round pipelines; 0 or 1 = first round.

    Position:
      type: object
      description: A schedulable interview template. Config internals are intentionally omitted.
      properties:
        id: { type: string, format: uuid }
        title: { type: string, examples: ["Senior Go Engineer"] }
        seniority: { $ref: '#/components/schemas/Seniority' }
        status:
          type: string
          enum: [open]
          description: Only open positions are schedulable and returned here.

    Interview:
      type: object
      properties:
        id: { type: string, format: uuid }
        status:
          type: string
          examples: [scheduled]
        invite_token: { type: string }
        invite_url:
          type: string
          format: uri
          description: Candidate-facing link to start the interview.
        role: { type: string }
        seniority: { $ref: '#/components/schemas/Seniority' }
        duration_min: { type: integer }
        scheduled_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        candidate: { $ref: '#/components/schemas/Candidate' }

    Report:
      type: object
      properties:
        interview_id: { type: string, format: uuid }
        overall_score:
          type: number
          format: float
          minimum: 0
          maximum: 100
        recommendation: { $ref: '#/components/schemas/Recommendation' }
        summary: { type: string }
        knowledge_summary: { type: string }
        questions_asked: { type: integer }
        duration_min:
          type: integer
          description: Candidate's actual elapsed minutes.
        proctor_flags: { type: integer }
        completed_at: { type: string, format: date-time }
        concept_depth:
          description: Per-concept knowledge-depth fingerprint (the product's core signal).
          type: array
          items: { type: object, additionalProperties: true }
        report_url:
          type: string
          format: uri
          description: Dashboard deep link to the full report.

    ReportReadyPayload:
      type: object
      properties:
        interview_id: { type: string, format: uuid }
        org_id: { type: string, format: uuid }
        candidate_id: { type: string, format: uuid }
        overall_score: { type: number, format: float }
        recommendation: { $ref: '#/components/schemas/Recommendation' }

    ApiError:
      type: object
      properties:
        code: { type: string, examples: ["forbidden"] }
        message: { type: string }
        details: {}

    # ── Envelopes ──
    InterviewEnvelope:
      type: object
      properties:
        success: { type: boolean, const: true }
        data: { $ref: '#/components/schemas/Interview' }

    PositionListEnvelope:
      type: object
      properties:
        success: { type: boolean, const: true }
        data:
          type: array
          items: { $ref: '#/components/schemas/Position' }

    ReportEnvelope:
      type: object
      properties:
        success: { type: boolean, const: true }
        data: { $ref: '#/components/schemas/Report' }

    ErrorEnvelope:
      type: object
      properties:
        success: { type: boolean, const: false }
        error: { $ref: '#/components/schemas/ApiError' }

    WebhookEnvelope:
      type: object
      description: The webhook delivery body.
      properties:
        id: { type: string, format: uuid, description: Unique event id. }
        type: { type: string, examples: ["report.generated"] }
        timestamp: { type: string, format: date-time }
        org_id: { type: string, format: uuid }
        data: { $ref: '#/components/schemas/ReportReadyPayload' }
