openapi: 3.1.0
info:
  title: Caspr API
  version: 1.0.0
  description: |
    The Caspr API provides programmatic access to call intelligence captured by the Caspr platform.

    Use the API to retrieve sessions, transcripts, post-call analysis (question classification, knowledge gaps, coaching feedback), structured call notes, QA pair battle cards, connectors, and aggregated analytics. You can also search the knowledge base and build integrations that react to call events in real time.

    **Base URL:** `https://caspr-copilot-prod.usepeppr.ai/api`
  contact:
    name: Caspr Support
    email: support@usepeppr.ai
    url: https://usepeppr.ai

servers:
  - url: https://caspr-copilot-prod.usepeppr.ai/api
    description: Production
  - url: http://localhost:8000/api
    description: Local development

security:
  - apiKey: []

tags:
  - name: Sessions
    description: Call sessions — the top-level container for transcripts, threads, analysis, and notes.
  - name: Transcripts
    description: Raw transcript lines for sessions.
  - name: Call Analysis
    description: Post-call classification of every detected customer question.
  - name: Call Notes
    description: Structured summaries and action items generated from the transcript.
  - name: QA Pairs
    description: Curated battle cards maintained by sales leadership.
  - name: Knowledge Base
    description: Connected data sources and semantic search.
  - name: Dashboard
    description: Aggregated analytics across sessions.
  - name: System
    description: Health checks and metadata.

paths:
  # ===========================================================================
  # Sessions
  # ===========================================================================

  /copilot-sessions:
    get:
      operationId: listSessions
      summary: List sessions
      description: |
        Returns a paginated list of call sessions. Each session represents a single customer call or meeting captured by Caspr. Results are scoped to the authenticated organization.
      tags: [Sessions]
      parameters:
        - name: organization_id
          in: query
          description: Filter by organization ID
          required: false
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          description: Filter by session status
          required: false
          schema:
            type: string
            enum: [active, inactive]
        - name: limit
          in: query
          description: Items per page
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          description: Items to skip
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Paginated list of sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedSessionsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /copilot-sessions/{session_id}:
    get:
      operationId: getSession
      summary: Get session details
      description: |
        Returns a session with its full transcript and conversation threads. Threads are the live-call Q&A interactions Caspr surfaced during the session.
      tags: [Sessions]
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session with transcript and threads
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionWithTranscript"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ===========================================================================
  # Transcripts
  # ===========================================================================

  /copilot-sessions/{session_id}/transcript:
    get:
      operationId: getTranscript
      summary: Get transcript
      description: |
        Returns the full transcript for a session as an ordered list of lines. Use `after_timestamp_ms` to fetch only lines newer than a given timestamp — useful for polling during active sessions.
      tags: [Transcripts]
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: after_timestamp_ms
          in: query
          description: Only return lines after this timestamp (milliseconds from session start)
          required: false
          schema:
            type: integer
      responses:
        "200":
          description: Ordered list of transcript lines
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TranscriptLine"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ===========================================================================
  # Call Analysis
  # ===========================================================================

  /call-analysis/session/{session_id}:
    get:
      operationId: getSessionAnalysis
      summary: Get call analysis
      description: |
        Returns the complete post-call analysis for a session — every customer question detected, how the rep responded, classification (correct, incorrect, knowledge_gap, deferred, unguarded), and coaching feedback. Returns `null` if analysis hasn't been generated yet.
      tags: [Call Analysis]
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Full call analysis with events, or null
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CallAnalysis"
                  - type: "null"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /call-analysis/session/{session_id}/summary:
    get:
      operationId: getSessionAnalysisSummary
      summary: Get analysis summary
      description: |
        Summary statistics for a session analysis — counts per classification, surfacing stats, and metadata. Returns the same header fields as the full analysis without the individual events array.
      tags: [Call Analysis]
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Summary statistics or null
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CallAnalysisSummary"
                  - type: "null"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /call-analysis/session/{session_id}/events:
    get:
      operationId: getSessionAnalysisEvents
      summary: Get analysis events
      description: |
        All individual question events for a session's analysis. Each event includes the detected question, rep response, classification, and knowledge base citations.
      tags: [Call Analysis]
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: List of events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CallAnalysisEvent"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /call-analysis/knowledge-gaps:
    get:
      operationId: listKnowledgeGaps
      summary: List knowledge gaps
      description: |
        Returns recent questions where no knowledge base content exists. Knowledge gaps are detected across all sessions for the authenticated organization. Useful for feeding your content team a prioritized list of gaps to close.
      tags: [Call Analysis]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: List of knowledge gap events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CallAnalysisEvent"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /call-analysis/coaching-queue:
    get:
      operationId: listCoachingQueue
      summary: List coaching items
      description: |
        Events needing rep coaching — incorrect answers, unnecessary deferrals, and unguarded responses (rep answered without knowledge base backing). Feed these to your coaching workflows.
      tags: [Call Analysis]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: List of coaching events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CallAnalysisEvent"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # Call Notes
  # ===========================================================================

  /call-notes/session/{session_id}:
    get:
      operationId: getSessionNotes
      summary: Get call notes
      description: |
        Returns structured notes for a session — summary headline, topic-based sections with markdown bullets, and action items with owners and due dates. Returns `null` if notes haven't been generated yet.
      tags: [Call Notes]
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Structured call notes, or null
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CallNotes"
                  - type: "null"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # QA Pairs
  # ===========================================================================

  /qa-pairs:
    get:
      operationId: listQAPairs
      summary: List QA pairs
      description: |
        Paginated list of curated QA battle cards for the authenticated organization.
      tags: [QA Pairs]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: search
          in: query
          description: Search in question and answer text
          schema:
            type: string
        - name: tag_id
          in: query
          description: Filter by tag IDs (can pass multiple)
          schema:
            type: array
            items:
              type: string
              format: uuid
      responses:
        "200":
          description: Paginated QA pairs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedQAPairsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createQAPair
      summary: Create a QA pair
      description: |
        Create a new curated QA battle card. Newly-created pairs are queued for indexing into the knowledge base.
      tags: [QA Pairs]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QAPairCreate"
      responses:
        "201":
          description: Created QA pair
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QAPair"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /qa-pairs/{qa_pair_id}:
    get:
      operationId: getQAPair
      summary: Get a QA pair
      tags: [QA Pairs]
      parameters:
        - $ref: "#/components/parameters/QAPairId"
      responses:
        "200":
          description: QA pair details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QAPair"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updateQAPair
      summary: Update a QA pair
      tags: [QA Pairs]
      parameters:
        - $ref: "#/components/parameters/QAPairId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/QAPairUpdate"
      responses:
        "200":
          description: Updated QA pair
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QAPair"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: deleteQAPair
      summary: Delete a QA pair
      tags: [QA Pairs]
      parameters:
        - $ref: "#/components/parameters/QAPairId"
      responses:
        "200":
          description: Deletion confirmation
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /qa-pairs/recommendations:
    get:
      operationId: listQARecommendations
      summary: List QA recommendations
      description: |
        QA pairs Caspr suggests creating based on patterns in recent calls — recurring knowledge gaps, unguarded answers that worked, or repeatedly-asked questions.
      tags: [QA Pairs]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: List of recommendations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/QARecommendation"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # Knowledge Base / Search
  # ===========================================================================

  /connectors:
    get:
      operationId: listConnectors
      summary: List connectors
      description: |
        List all configured data source connectors for the authenticated user.
      tags: [Knowledge Base]
      responses:
        "200":
          description: List of connectors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Connector"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /connectors/{connector_id}:
    get:
      operationId: getConnector
      summary: Get a connector
      tags: [Knowledge Base]
      parameters:
        - name: connector_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Connector details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connector"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ===========================================================================
  # Dashboard
  # ===========================================================================

  /dashboard/analytics:
    get:
      operationId: getDashboardAnalytics
      summary: Get dashboard analytics
      description: |
        Aggregated analytics across sessions for a date range — stat cards, classification breakdown, top knowledge gaps, recent sessions, and team-level stats (admin keys only).
      tags: [Dashboard]
      parameters:
        - name: from
          in: query
          description: Start date (YYYY-MM-DD). Defaults to 7 days ago.
          schema:
            type: string
            format: date
        - name: to
          in: query
          description: End date (YYYY-MM-DD). Defaults to today.
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Dashboard analytics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DashboardAnalytics"
        "400":
          description: Invalid date range
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # System
  # ===========================================================================

  /ping:
    get:
      operationId: healthCheck
      summary: Health check
      description: Simple health check. No authentication required.
      tags: [System]
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ping:
                    type: string
                    example: pong

# =============================================================================
# Components
# =============================================================================

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Pass your API key in the `Authorization` header:
        ```
        Authorization: Bearer pk_live_...
        ```

  parameters:
    SessionId:
      name: session_id
      in: path
      required: true
      description: Unique session identifier
      schema:
        type: string
        format: uuid
    QAPairId:
      name: qa_pair_id
      in: path
      required: true
      description: Unique QA pair identifier
      schema:
        type: string
        format: uuid

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            type: object
            properties:
              detail:
                type: string
                example: Not authenticated
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              detail:
                type: string
                example: Session not found

  schemas:
    # =========================================================================
    # Sessions
    # =========================================================================

    Session:
      type: object
      description: A call or meeting captured by Caspr
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [active, inactive]
          description: |
            - `active` — call is currently in progress
            - `inactive` — call has ended
        source:
          type: string
          enum: [live, uploaded]
          description: |
            - `live` — recorded in real time
            - `uploaded` — created from an uploaded audio file
        title:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        ended_at:
          type: string
          format: date-time
          nullable: true
        playbook_id:
          type: string
          format: uuid
          nullable: true
        product_profile_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - user_id
        - organization_id
        - status
        - source
        - created_at
        - updated_at

    SessionWithTranscript:
      allOf:
        - $ref: "#/components/schemas/Session"
        - type: object
          properties:
            transcript_lines:
              type: array
              items:
                $ref: "#/components/schemas/TranscriptLine"
            threads:
              type: array
              items:
                $ref: "#/components/schemas/Thread"

    PaginatedSessionsResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/Session"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        has_more:
          type: boolean
      required: [sessions, total, limit, offset, has_more]

    TranscriptLine:
      type: object
      description: A single line of speech from the transcript
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        speaker:
          type: string
          enum: [prospect, rep, system, unknown]
        speaker_name:
          type: string
          nullable: true
        text:
          type: string
          example: How does your data retention policy work?
        timestamp_ms:
          type: integer
          example: 142500
        confidence:
          type: number
          nullable: true
          example: 0.96
      required: [id, session_id, speaker, text, timestamp_ms]

    Thread:
      type: object
      description: A live-call Q&A thread created when Caspr detected a customer question
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        title:
          type: string
        category:
          type: string
          nullable: true
          enum: [competitive, technical, pricing, process, policy, product, general]
        has_unread:
          type: boolean
        messages:
          type: array
          items:
            $ref: "#/components/schemas/ThreadMessage"
        message_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required: [id, session_id, title, messages, message_count, created_at, updated_at]

    ThreadMessage:
      type: object
      properties:
        id:
          type: string
        role:
          type: string
          enum: [user, assistant, system]
        content:
          type: string
        spoken_text:
          type: string
          nullable: true
          description: Verbatim transcript text (user messages only)
        timestamp:
          type: string
          format: date-time
        sources:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/Citation"
        header_text:
          type: string
          nullable: true
        details_text:
          type: string
          nullable: true
        answer_source:
          type: string
          nullable: true
          enum: [qa_cards, knowledge_base]
        latency_ms:
          type: integer
          nullable: true
      required: [id, role, content, timestamp]

    Citation:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        sourceUrl:
          type: string
          nullable: true
        snippet:
          type: string
        confidence:
          type: number
        pageNumber:
          type: integer
          nullable: true

    # =========================================================================
    # Call Analysis
    # =========================================================================

    CallAnalysis:
      type: object
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        total_events:
          type: integer
        correct_count:
          type: integer
        incorrect_count:
          type: integer
        knowledge_gap_count:
          type: integer
        deferred_count:
          type: integer
        unguarded_count:
          type: integer
        surfaced_events:
          type: integer
        not_surfaced_events:
          type: integer
        analyzed_at:
          type: string
          format: date-time
          nullable: true
        events:
          type: array
          items:
            $ref: "#/components/schemas/CallAnalysisEvent"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - session_id
        - total_events
        - correct_count
        - incorrect_count
        - knowledge_gap_count
        - deferred_count
        - unguarded_count
        - surfaced_events
        - not_surfaced_events
        - events
        - created_at
        - updated_at

    CallAnalysisSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        total_events:
          type: integer
        correct_count:
          type: integer
        incorrect_count:
          type: integer
        knowledge_gap_count:
          type: integer
        deferred_count:
          type: integer
        unguarded_count:
          type: integer
        surfaced_events:
          type: integer
        not_surfaced_events:
          type: integer
        analyzed_at:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - session_id
        - total_events
        - correct_count
        - incorrect_count
        - knowledge_gap_count
        - deferred_count
        - unguarded_count
        - surfaced_events
        - not_surfaced_events

    CallAnalysisEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        call_analysis_id:
          type: string
          format: uuid
        start_timestamp_ms:
          type: number
        end_timestamp_ms:
          type: number
        spoken_text:
          type: string
        rewritten_question:
          type: string
          example: What is the data retention policy?
        rag_queries:
          type: array
          nullable: true
          items:
            type: string
        reason:
          type: string
          nullable: true
        knowledge_category:
          type: string
          nullable: true
          enum: [product_technical, business_sales]
        response:
          type: string
          nullable: true
        knowledge_exists:
          type: boolean
        knowledge_confidence:
          type: number
          nullable: true
        knowledge_sources:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/Citation"
        classification:
          type: string
          enum: [correct, incorrect, knowledge_gap, deferred, unguarded]
          description: |
            - `correct` — response aligns with knowledge base
            - `incorrect` — response contradicts knowledge base
            - `knowledge_gap` — no relevant content in KB
            - `deferred` — rep deferred or didn't respond
            - `unguarded` — rep answered without KB backing
        comments:
          type: string
          nullable: true
          description: AI-generated coaching feedback
        was_surfaced:
          type: boolean
          description: Whether Caspr showed this question to the rep live
        thread_id:
          type: string
          format: uuid
          nullable: true
        answer_source:
          type: string
          nullable: true
          enum: [qa_cards, knowledge_base]
        surfaced_answer:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - call_analysis_id
        - start_timestamp_ms
        - end_timestamp_ms
        - spoken_text
        - rewritten_question
        - knowledge_exists
        - classification
        - was_surfaced
        - created_at
        - updated_at

    # =========================================================================
    # Call Notes
    # =========================================================================

    CallNotes:
      type: object
      properties:
        id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        summary:
          type: string
          example: "Discovery call with Acme Corp. Prospect is interested but raised concerns about implementation cost and integration timeline."
        sections:
          type: array
          items:
            $ref: "#/components/schemas/CallNotesSection"
        actionable_todos:
          type: array
          items:
            $ref: "#/components/schemas/ActionItem"
        generated_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - session_id
        - summary
        - sections
        - actionable_todos
        - generated_at
        - created_at
        - updated_at

    CallNotesSection:
      type: object
      properties:
        title:
          type: string
          example: Current Tech Stack
        content:
          type: string
          example: "- Uses **Salesforce** as primary CRM\n- Evaluating **Gong** for call recording\n- Integration with **Slack** is a requirement"
      required: [title, content]

    ActionItem:
      type: object
      properties:
        description:
          type: string
          example: Send the Q3 security compliance PDF to the prospect
        owner:
          type: string
          nullable: true
          example: Rep
        due_date:
          type: string
          nullable: true
          example: by Friday
      required: [description]

    # =========================================================================
    # QA Pairs
    # =========================================================================

    QAPair:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        organization_id:
          type: string
          format: uuid
        question:
          type: string
          example: How does pricing work?
        answer:
          type: string
          example: Per-seat, billed annually. Standard tier is $20/user/month.
        question_variations:
          type: array
          maxItems: 5
          items:
            type: string
        tag_id:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
        index_status:
          type: string
          enum: [pending, dirty, indexed, failed]
        tag:
          nullable: true
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            color_index:
              type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - user_id
        - organization_id
        - question
        - answer
        - is_active
        - index_status
        - created_at
        - updated_at

    QAPairCreate:
      type: object
      required: [question, answer]
      properties:
        question:
          type: string
        answer:
          type: string
        question_variations:
          type: array
          maxItems: 5
          items:
            type: string
        tag_id:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
          default: true

    QAPairUpdate:
      type: object
      properties:
        question:
          type: string
          nullable: true
        answer:
          type: string
          nullable: true
        question_variations:
          type: array
          maxItems: 5
          items:
            type: string
          nullable: true
        tag_id:
          type: string
          format: uuid
          nullable: true
        is_active:
          type: boolean
          nullable: true

    PaginatedQAPairsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/QAPair"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        has_more:
          type: boolean
      required: [items, total, limit, offset, has_more]

    QARecommendation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        question:
          type: string
        source_type:
          type: string
          enum: [knowledge_gap, unguarded, rag_sourced]
        status:
          type: string
          enum: [active, dismissed, converted, stale]
        occurrence_count:
          type: integer
        sample_response:
          type: string
          nullable: true
        knowledge_sources:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/Citation"
        knowledge_category:
          type: string
          nullable: true
        first_seen:
          type: string
          format: date-time
        last_seen:
          type: string
          format: date-time
        event_ids:
          type: array
          items:
            type: string
      required: [id, question, source_type, status, occurrence_count, first_seen, last_seen, event_ids]

    # =========================================================================
    # Knowledge Base
    # =========================================================================

    Connector:
      type: object
      description: A configured connection to an external data source
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: Engineering Confluence
        source:
          type: string
          description: Document source type (e.g., google_drive, confluence, notion, github)
        input_type:
          type: string
          enum: [load_state, poll]
        connector_specific_config:
          type: object
          additionalProperties: true
        refresh_freq:
          type: integer
          nullable: true
          description: Sync frequency in seconds
        credential_id:
          type: string
          format: uuid
          nullable: true
        status:
          type: string
          description: Sync status
        target_folder:
          type: string
          enum: [enterprise_search, copilot, general]
          description: |
            - `copilot` — used during live calls and post-call analysis
            - `enterprise_search` — used for standalone semantic search
            - `general` — used in both contexts
        last_successful_index_time:
          type: string
          format: date-time
          nullable: true
        total_docs_indexed:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    # =========================================================================
    # Dashboard
    # =========================================================================

    DashboardAnalytics:
      type: object
      properties:
        stats:
          type: object
          properties:
            total_calls:
              $ref: "#/components/schemas/StatCard"
            questions_fielded:
              $ref: "#/components/schemas/StatCard"
            accuracy_rate:
              $ref: "#/components/schemas/StatCard"
            knowledge_gaps_found:
              $ref: "#/components/schemas/StatCard"
        classification_breakdown:
          type: object
          properties:
            correct:
              type: integer
            incorrect:
              type: integer
            knowledge_gap:
              type: integer
            deferred:
              type: integer
            unguarded:
              type: integer
            total:
              type: integer
        top_knowledge_gaps:
          type: array
          items:
            type: object
            properties:
              question:
                type: string
              occurrences:
                type: integer
              category:
                type: string
                nullable: true
        recent_sessions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              title:
                type: string
              date:
                type: string
              accuracy_percent:
                type: number
                nullable: true
              total_questions:
                type: integer

    StatCard:
      type: object
      properties:
        label:
          type: string
        value:
          type: number
        formatted_value:
          type: string
          example: "87%"
        change:
          type: number
          nullable: true
        change_label:
          type: string
          nullable: true
