openapi: 3.0.3
info:
  title: Sophia API
  description: |
    Best-effort OpenAPI generated from Hono routes in `apps/api/src/modules/*/` and Zod contracts in `packages/shared/src/contracts/`.

    **Gaps:**
    - Bot GET context / POST start responses are service-defined only (see `bot-interview.service.ts`).
    - Path `:id` params without route-level Zod may accept invalid ObjectIds until the service layer.
    - `GET /v1/reports/{interviewId}` may return a lean Mongo document not fully aligned with `ReportResponse`.
  version: 1.0.0
servers:
  - url: http://localhost:3000
    description: Local development
  - url: https://api.sophia.cleverhire.ai
    description: Production
  - url: https://develop-api.sophia.cleverhire.ai
    description: Development stage

tags:
  - name: Health
  - name: Auth
  - name: Team
  - name: Positions
  - name: Candidates
  - name: Files
  - name: Interviews
  - name: CandidateInterview
  - name: BotInterview
  - name: Notes
  - name: Credits
  - name: Webhooks
  - name: Reports

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Firebase ID token
    CandidateSession:
      type: apiKey
      in: header
      name: x-candidate-session
    BotInternal:
      type: apiKey
      in: header
      name: x-internal-token
    StripeSignature:
      type: apiKey
      in: header
      name: stripe-signature

  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    ObjectIdPath:
      name: id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ObjectId'

  schemas:
    ObjectId:
      type: string
      pattern: '^[0-9a-fA-F]{24}$'

    ErrorResponse:
      type: object
      required: [error, code, requestId]
      properties:
        error:
          type: string
        code:
          type: string
          enum:
            - VALIDATION_FAILED
            - MALFORMED_ID
            - UNAUTHENTICATED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - ALREADY_EXISTS
            - INSUFFICIENT_CREDITS
            - RATE_LIMITED
            - INTERNAL
            - DEPENDENCY_UNAVAILABLE
        requestId:
          type: string
        details:
          nullable: true

    ValidationDetail:
      type: object
      properties:
        path:
          type: string
        message:
          type: string

    PaginatedMeta:
      type: object
      required: [docs, page, limit, total, hasMore]
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        hasMore:
          type: boolean

    HealthResponse:
      type: object
      required: [status, version, timestamp, uptimeSeconds, dependencies]
      properties:
        status:
          type: string
          enum: [ok, degraded]
        version:
          type: string
        timestamp:
          type: string
        uptimeSeconds:
          type: number
        dependencies:
          type: object
          properties:
            mongo:
              type: string
              enum: [up, down, unknown]

    HelloResponse:
      type: object
      properties:
        message:
          type: string
        requestId:
          type: string
        servedAt:
          type: string

    UserResponse:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ObjectId'
        organizationId:
          $ref: '#/components/schemas/ObjectId'
        email:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        role:
          type: string
          enum: [admin, member]
        status:
          type: string
          enum: [invited, active, disabled]
        lastLoginAt:
          type: string
          nullable: true
        invitedBy:
          $ref: '#/components/schemas/ObjectId'
          nullable: true
        invitedAt:
          type: string
          nullable: true
        createdAt:
          type: string
        updatedAt:
          type: string

    OrganizationResponse:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ObjectId'
        name:
          type: string
        slug:
          type: string
        status:
          type: string
          enum: [active, suspended]
        branding:
          type: object
          additionalProperties: true
        settings:
          type: object
          properties:
            interviewDurationCapMinutes:
              type: integer
            creditsPerInterviewMinute:
              type: integer
            defaultPositionDurationMinutes:
              type: integer
            defaultExpiryWindowDays:
              type: integer
        timezone:
          type: string
          nullable: true
        credits:
          type: object
          properties:
            available:
              type: integer
            reserved:
              type: integer
        trialCreditsGranted:
          type: boolean
        createdBy:
          $ref: '#/components/schemas/ObjectId'
        createdAt:
          type: string
        updatedAt:
          type: string

    SignupRequest:
      type: object
      required: [workspaceName, firstName, lastName, email, password]
      properties:
        workspaceName:
          type: string
          minLength: 2
          maxLength: 50
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8

    GoogleSignupRequest:
      type: object
      required: [idToken, workspaceName]
      properties:
        idToken:
          type: string
        workspaceName:
          type: string

    SignupResponse:
      type: object
      properties:
        user:
          $ref: '#/components/schemas/UserResponse'
        organization:
          $ref: '#/components/schemas/OrganizationResponse'
        token:
          type: string

    MessageResponse:
      type: object
      properties:
        message:
          type: string

    PositionSkill:
      type: object
      required: [name]
      properties:
        name:
          type: string
        context:
          type: string
        importance:
          type: string
          enum: [high, medium, low]
        mustHave:
          type: boolean

    PositionResponse:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ObjectId'
        organizationId:
          $ref: '#/components/schemas/ObjectId'
        title:
          type: string
        seniority:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        aiContext:
          type: object
          required: [roleIntroduction, experienceLevel]
        skills:
          type: array
          items:
            $ref: '#/components/schemas/PositionSkill'
        durationMinutes:
          type: integer
        expiryDays:
          type: integer
        status:
          type: string
          enum: [draft, active, archived]
        createdBy:
          $ref: '#/components/schemas/ObjectId'
        createdAt:
          type: string
        updatedAt:
          type: string

    PositionDetailResponse:
      allOf:
        - $ref: '#/components/schemas/PositionResponse'
        - type: object
          properties:
            interviewCount:
              type: integer

    ListPositionsResponse:
      allOf:
        - $ref: '#/components/schemas/PaginatedMeta'
        - type: object
          properties:
            docs:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/PositionResponse'
                  - type: object
                    properties:
                      interviewCount:
                        type: integer

    CandidateResponse:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ObjectId'
        organizationId:
          $ref: '#/components/schemas/ObjectId'
        email:
          type: string
        firstName:
          type: string
        lastName:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        status:
          type: string
          enum: [active, blacklisted]
        notes:
          type: string
          nullable: true
        resume:
          type: object
          nullable: true
        skills:
          type: array
          items:
            type: string
        source:
          type: string
        tags:
          type: array
          items:
            type: string
        createdBy:
          $ref: '#/components/schemas/ObjectId'
        createdAt:
          type: string
        updatedAt:
          type: string

    CandidateListResponse:
      allOf:
        - $ref: '#/components/schemas/PaginatedMeta'
        - type: object
          properties:
            docs:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/CandidateResponse'
                  - type: object
                    properties:
                      interviewCount:
                        type: integer

    ResumeUploadUrlResponse:
      type: object
      properties:
        url:
          type: string
        key:
          type: string
        expiresInSeconds:
          type: integer

    ResumeParseResponse:
      type: object
      additionalProperties: false
      properties:
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
        phone:
          type: string
        location:
          type: string
        skills:
          type: array
          items:
            type: string
        totalExperienceMonths:
          type: integer
        currentTitle:
          type: string
        currentCompany:
          type: string
        education:
          type: string

    InterviewResponse:
      type: object
      description: See packages/shared/src/contracts/interview.ts interviewResponseSchema
      additionalProperties: true

    ScheduleInterviewResponse:
      type: object
      properties:
        interview:
          $ref: '#/components/schemas/InterviewResponse'
        joinUrl:
          type: string

    InterviewStatusCounts:
      type: object
      properties:
        all:
          type: integer
        scheduled:
          type: integer
        inProgress:
          type: integer
        completed:
          type: integer
        expired:
          type: integer
        cancelled:
          type: integer
        failed:
          type: integer
        abandoned:
          type: integer
        disconnected:
          type: integer

    CreditQuoteResponse:
      type: object
      properties:
        cost:
          type: number
        available:
          type: number
        remainingAfter:
          type: number
        sufficient:
          type: boolean

    CopyLinkResponse:
      type: object
      properties:
        joinUrl:
          type: string
        accessPassword:
          type: string
        message:
          type: string

    CandidatePeekResponse:
      type: object
      properties:
        positionTitle:
          type: string
        candidateFirstName:
          type: string
        branding:
          type: object

    VerifyCandidateResponse:
      type: object
      properties:
        sessionToken:
          type: string
        expiresAt:
          type: string

    CandidateInterviewResponse:
      type: object
      properties:
        interviewId:
          type: string
        status:
          type: string
        positionTitle:
          type: string
        durationMinutes:
          type: integer
        candidateFirstName:
          type: string
        enableRecording:
          type: boolean
        recordingConsentAt:
          type: string
          nullable: true
        branding:
          type: object

    CandidateBotSessionResponse:
      type: object
      properties:
        connectUrl:
          type: string
        token:
          type: string
        expiresAt:
          type: string

    BotInterviewIdStatus:
      type: object
      properties:
        id:
          type: string
        status:
          type: string

    BotErrorReportResponse:
      type: object
      properties:
        received:
          type: boolean

    NoteResponse:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/ObjectId'
        organizationId:
          $ref: '#/components/schemas/ObjectId'
        targetType:
          type: string
          enum: [candidate, interview]
        targetId:
          $ref: '#/components/schemas/ObjectId'
        content:
          type: string
        createdBy:
          $ref: '#/components/schemas/ObjectId'
        authorName:
          type: string
        editedAt:
          type: string
          nullable: true
        createdAt:
          type: string
        updatedAt:
          type: string

    CreditSummaryResponse:
      type: object
      properties:
        available:
          type: integer
        reserved:
          type: integer
        usedThisMonth:
          type: integer
        recentLedger:
          type: array
          items:
            type: object
            additionalProperties: true

    CheckoutResponse:
      type: object
      properties:
        url:
          type: string
          format: uri

    StripeWebhookResponse:
      type: object
      properties:
        received:
          type: boolean

    ReportStatusResponse:
      type: object
      properties:
        status:
          type: string
          enum: [not_started, processing, completed, failed]
        attemptCount:
          type: integer
        reportFailureReason:
          type: string

    ReportResponse:
      type: object
      description: Intended shape from reportResponseSchema; GET handler may return raw Mongo lean doc
      additionalProperties: true

paths:
  /health:
    get:
      tags: [Health]
      summary: Liveness and dependency status
      operationId: getHealth
      responses:
        '200':
          description: OK or degraded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /v1/hello:
    get:
      tags: [Health]
      summary: Smoke test endpoint
      operationId: getHello
      parameters:
        - name: name
          in: query
          schema:
            type: string
            default: world
            minLength: 1
            maxLength: 80
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HelloResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/auth/signup:
    post:
      tags: [Auth]
      summary: Email/password signup
      operationId: postAuthSignup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignupRequest'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/auth/signup/google:
    post:
      tags: [Auth]
      summary: Google signup
      operationId: postAuthSignupGoogle
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GoogleSignupRequest'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'

  /v1/auth/verify-email:
    post:
      tags: [Auth]
      summary: Verify email with Firebase oob code
      operationId: postAuthVerifyEmail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [oobCode]
              properties:
                oobCode:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/auth/resend-verification:
    post:
      tags: [Auth]
      summary: Resend verification email
      operationId: postAuthResendVerification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/auth/me:
    get:
      tags: [Auth]
      summary: Current session user and organization
      operationId: getAuthMe
      security:
        - BearerAuth: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/UserResponse'
                  organization:
                    $ref: '#/components/schemas/OrganizationResponse'
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/auth/logout:
    post:
      tags: [Auth]
      summary: Logout (revoke refresh tokens)
      operationId: postAuthLogout
      security:
        - BearerAuth: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/team/invite/{token}:
    get:
      tags: [Team]
      summary: Public invite details
      operationId: getTeamInviteDetails
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/team/invite/{token}/accept:
    post:
      tags: [Team]
      summary: Accept invite with password
      operationId: postTeamInviteAccept
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [firstName, lastName, password]
              properties:
                firstName:
                  type: string
                lastName:
                  type: string
                password:
                  type: string
                  minLength: 8
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'

  /v1/team/invite/{token}/accept/google:
    post:
      tags: [Team]
      summary: Accept invite with Google
      operationId: postTeamInviteAcceptGoogle
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [idToken]
              properties:
                idToken:
                  type: string
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignupResponse'

  /v1/team:
    get:
      tags: [Team]
      summary: List team members
      operationId: getTeam
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedMeta'
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: '#/components/schemas/UserResponse'

  /v1/team/invite:
    post:
      tags: [Team]
      summary: Invite a team member (admin)
      operationId: postTeamInvite
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, firstName, lastName, role]
              properties:
                email:
                  type: string
                firstName:
                  type: string
                lastName:
                  type: string
                role:
                  type: string
                  enum: [admin, member]
      responses:
        '201':
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/UserResponse'
                  inviteLink:
                    type: string
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/team/{userId}/role:
    patch:
      tags: [Team]
      summary: Change member role (admin)
      operationId: patchTeamUserRole
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  type: string
                  enum: [admin, member]
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/UserResponse'

  /v1/team/{userId}/deactivate:
    post:
      tags: [Team]
      summary: Deactivate member (admin)
      operationId: postTeamDeactivate
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/team/{userId}/reactivate:
    post:
      tags: [Team]
      summary: Reactivate member (admin)
      operationId: postTeamReactivate
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/team/{userId}/resend-invite:
    post:
      tags: [Team]
      summary: Resend invite (admin)
      operationId: postTeamResendInvite
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  inviteLink:
                    type: string

  /v1/team/{userId}/revoke-invite:
    post:
      tags: [Team]
      summary: Revoke pending invite (admin)
      operationId: postTeamRevokeInvite
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/positions:
    get:
      tags: [Positions]
      summary: List positions
      operationId: getPositions
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, active, archived]
        - name: search
          in: query
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListPositionsResponse'
    post:
      tags: [Positions]
      summary: Create position
      operationId: postPositions
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionResponse'

  /v1/positions/{id}:
    get:
      tags: [Positions]
      summary: Get position
      operationId: getPositionById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionDetailResponse'
    patch:
      tags: [Positions]
      summary: Update position
      operationId: patchPositionById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionResponse'

  /v1/positions/{id}/archive:
    post:
      tags: [Positions]
      summary: Archive position
      operationId: postPositionArchive
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/candidates:
    get:
      tags: [Candidates]
      summary: List candidates
      operationId: getCandidates
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
        - name: tags
          in: query
          schema:
            oneOf:
              - type: string
              - type: array
                items:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateListResponse'
    post:
      tags: [Candidates]
      summary: Create candidate
      operationId: postCandidates
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'

  /v1/candidates/{id}:
    get:
      tags: [Candidates]
      summary: Get candidate
      operationId: getCandidateById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'
    patch:
      tags: [Candidates]
      summary: Update candidate
      operationId: patchCandidateById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'

  /v1/candidates/{id}/blacklist:
    post:
      tags: [Candidates]
      summary: Blacklist candidate
      operationId: postCandidateBlacklist
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'

  /v1/candidates/{id}/activate:
    post:
      tags: [Candidates]
      summary: Activate candidate
      operationId: postCandidateActivate
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'

  /v1/candidates/{id}/resume:
    get:
      tags: [Candidates]
      summary: Resume download URL
      operationId: getCandidateResume
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
        - name: inline
          in: query
          schema:
            type: boolean
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                  fileName:
                    type: string
                  expiresInSeconds:
                    type: integer
    post:
      tags: [Candidates]
      summary: Attach resume
      operationId: postCandidateResume
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resumeKey]
              properties:
                resumeKey:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateResponse'

  /v1/files/resume-upload-url:
    post:
      tags: [Files]
      summary: Presigned resume upload URL
      operationId: postResumeUploadUrl
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileName, fileType, fileSize]
              properties:
                fileName:
                  type: string
                fileType:
                  type: string
                  enum:
                    - application/pdf
                    - application/vnd.openxmlformats-officedocument.wordprocessingml.document
                fileSize:
                  type: integer
                  maximum: 10485760
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResumeUploadUrlResponse'

  /v1/files/resume-parse:
    post:
      tags: [Files]
      summary: Parse uploaded resume (no DB write)
      operationId: postResumeParse
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resumeKey]
              properties:
                resumeKey:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResumeParseResponse'

  /v1/interviews/status-counts:
    get:
      tags: [Interviews]
      summary: Interview counts by status
      operationId: getInterviewStatusCounts
      security:
        - BearerAuth: []
      parameters:
        - name: search
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
        - name: positionId
          in: query
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: candidateId
          in: query
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewStatusCounts'

  /v1/interviews/credit-quote:
    get:
      tags: [Interviews]
      summary: Credit quote for duration
      operationId: getInterviewCreditQuote
      security:
        - BearerAuth: []
      parameters:
        - name: durationMinutes
          in: query
          required: true
          schema:
            type: integer
            minimum: 5
            maximum: 60
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditQuoteResponse'

  /v1/interviews:
    get:
      tags: [Interviews]
      summary: List interviews
      operationId: getInterviews
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema:
            type: string
        - name: positionId
          in: query
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: candidateId
          in: query
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: search
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedMeta'
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: '#/components/schemas/InterviewResponse'
    post:
      tags: [Interviews]
      summary: Schedule interview
      operationId: postInterviews
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [positionId, candidateId, expiryDays]
              properties:
                positionId:
                  $ref: '#/components/schemas/ObjectId'
                candidateId:
                  $ref: '#/components/schemas/ObjectId'
                durationMinutes:
                  type: integer
                expiryDays:
                  type: integer
                  enum: [7, 14]
                metadata:
                  type: string
                enableRecording:
                  type: boolean
                  default: true
                sendNotification:
                  type: boolean
                  default: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleInterviewResponse'
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/interviews/{id}:
    get:
      tags: [Interviews]
      summary: Get interview
      operationId: getInterviewById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewResponse'
    patch:
      tags: [Interviews]
      summary: Update scheduled interview
      operationId: patchInterviewById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewResponse'
    delete:
      tags: [Interviews]
      summary: Delete interview (admin)
      operationId: deleteInterviewById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/interviews/{id}/cancel:
    post:
      tags: [Interviews]
      summary: Cancel interview
      operationId: postInterviewCancel
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/interviews/{id}/resend:
    post:
      tags: [Interviews]
      summary: Resend invitation
      operationId: postInterviewResend
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'

  /v1/interviews/{id}/copy-link:
    post:
      tags: [Interviews]
      summary: Copy join link and password
      operationId: postInterviewCopyLink
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CopyLinkResponse'

  /v1/join/{token}/peek:
    get:
      tags: [CandidateInterview]
      summary: Join page preview
      operationId: getJoinPeek
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidatePeekResponse'

  /v1/join/{token}/verify:
    post:
      tags: [CandidateInterview]
      summary: Verify interview password
      operationId: postJoinVerify
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCandidateResponse'

  /v1/candidate/interview:
    get:
      tags: [CandidateInterview]
      summary: Candidate lobby data
      operationId: getCandidateInterview
      security:
        - CandidateSession: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateInterviewResponse'

  /v1/candidate/consent:
    post:
      tags: [CandidateInterview]
      summary: Record recording consent
      operationId: postCandidateConsent
      security:
        - CandidateSession: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  recordingConsentAt:
                    type: string

  /v1/candidate/status:
    get:
      tags: [CandidateInterview]
      summary: Poll interview status
      operationId: getCandidateStatus
      security:
        - CandidateSession: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/candidate/bot-session:
    post:
      tags: [CandidateInterview]
      summary: Bot connect credentials
      operationId: postCandidateBotSession
      security:
        - CandidateSession: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandidateBotSessionResponse'

  /v1/bot/interviews/{id}/context:
    get:
      tags: [BotInterview]
      summary: Bot interview context
      operationId: getBotInterviewContext
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          description: See bot-interview.service getInterviewContext
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/bot/interviews/{id}/start:
    post:
      tags: [BotInterview]
      summary: Start interview and create Daily room
      operationId: postBotInterviewStart
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          description: See bot-interview.service startInterview
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/bot/interviews/{id}/runtime:
    patch:
      tags: [BotInterview]
      summary: Update runtime fields
      operationId: patchBotInterviewRuntime
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                remainingSeconds:
                  type: integer
                lastActiveAt:
                  type: string
                  format: date-time
                recordingDailyRecordingId:
                  type: string
                recordingStatus:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/disconnect:
    post:
      tags: [BotInterview]
      summary: Disconnect interview
      operationId: postBotInterviewDisconnect
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/rejoin:
    post:
      tags: [BotInterview]
      summary: Rejoin disconnected interview
      operationId: postBotInterviewRejoin
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/complete:
    post:
      tags: [BotInterview]
      summary: Complete interview
      operationId: postBotInterviewComplete
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [completedAt, actualSeconds]
              properties:
                completedAt:
                  type: string
                  format: date-time
                actualSeconds:
                  type: integer
                recordingUrl:
                  type: string
                  format: uri
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/fail:
    post:
      tags: [BotInterview]
      summary: Fail interview
      operationId: postBotInterviewFail
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [failureReason]
              properties:
                failureReason:
                  type: string
                  enum: [none, systemError, manualError]
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/error:
    post:
      tags: [BotInterview]
      summary: Report bot error
      operationId: postBotInterviewError
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [errorCategory, errorCode, severity, message, recoverable]
              properties:
                errorCategory:
                  type: string
                errorCode:
                  type: string
                severity:
                  type: string
                message:
                  type: string
                recoverable:
                  type: boolean
                metadata:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotErrorReportResponse'

  /v1/bot/interviews/{id}/proctoring:
    patch:
      tags: [BotInterview]
      summary: Update proctoring map
      operationId: patchBotInterviewProctoring
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/bot/interviews/{id}/cost:
    patch:
      tags: [BotInterview]
      summary: Update cost metrics
      operationId: patchBotInterviewCost
      security:
        - BotInternal: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BotInterviewIdStatus'

  /v1/notes:
    get:
      tags: [Notes]
      summary: List notes for target
      operationId: getNotes
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: targetType
          in: query
          required: true
          schema:
            type: string
            enum: [candidate, interview]
        - name: targetId
          in: query
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedMeta'
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: '#/components/schemas/NoteResponse'
    post:
      tags: [Notes]
      summary: Create note
      operationId: postNotes
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [targetType, targetId, content]
              properties:
                targetType:
                  type: string
                  enum: [candidate, interview]
                targetId:
                  $ref: '#/components/schemas/ObjectId'
                content:
                  type: string
                  minLength: 1
                  maxLength: 5000
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteResponse'

  /v1/notes/{id}:
    patch:
      tags: [Notes]
      summary: Update note
      operationId: patchNoteById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NoteResponse'
    delete:
      tags: [Notes]
      summary: Delete note
      operationId: deleteNoteById
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ObjectIdPath'
      responses:
        '204':
          description: No content

  /v1/credits:
    get:
      tags: [Credits]
      summary: Credit summary (admin)
      operationId: getCredits
      security:
        - BearerAuth: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditSummaryResponse'
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/credits/history:
    get:
      tags: [Credits]
      summary: Paginated credit ledger (admin)
      operationId: getCreditsHistory
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: type
          in: query
          schema:
            type: string
        - name: from
          in: query
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          schema:
            type: string
            format: date-time
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedMeta'
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          type: object
                          additionalProperties: true

  /v1/credits/lots:
    get:
      tags: [Credits]
      summary: Paginated credit lots (admin)
      operationId: getCreditsLots
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedMeta'
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          type: object
                          additionalProperties: true

  /v1/credits/checkout:
    post:
      tags: [Credits]
      summary: Create Stripe Checkout session (admin)
      operationId: postCreditsCheckout
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packId]
              properties:
                packId:
                  type: string
                  enum: [starter, popular, pro]
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'

  /v1/webhooks/stripe:
    post:
      tags: [Webhooks]
      summary: Stripe webhook receiver
      operationId: postStripeWebhook
      security:
        - StripeSignature: []
      requestBody:
        required: true
        content:
          text/plain:
            schema:
              type: string
              description: Raw Stripe event body
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeWebhookResponse'
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/reports/{interviewId}:
    get:
      tags: [Reports]
      summary: Get report by interview id
      operationId: getReportByInterviewId
      security:
        - BearerAuth: []
      parameters:
        - name: interviewId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResponse'
        '404':
          description: Inline not found (may omit requestId)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  code:
                    type: string

  /v1/reports/{interviewId}/status:
    get:
      tags: [Reports]
      summary: Report generation status
      operationId: getReportStatus
      security:
        - BearerAuth: []
      parameters:
        - name: interviewId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportStatusResponse'

  /v1/reports/{interviewId}/retry:
    post:
      tags: [Reports]
      summary: Retry report generation (admin)
      operationId: postReportRetry
      security:
        - BearerAuth: []
      parameters:
        - name: interviewId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
