openapi: 3.0.3
info:
  title: Agnost AI API
  description: |
    Complete REST API for Agnost AI: an analytics and monitoring platform for AI agents.
    
    **Authentication**:
    - SDK ingestion endpoints (`/api/v1/*`) use `x-org-id` header (UUID)
    - Dashboard endpoints use `Authorization: Bearer <jwt>` + `x-org-id` header, or `x-api-key` header
    - API key management uses JWT only (no API key auth)
  version: 2.1.3
  contact:
    name: Agnost AI
    url: https://agnost.ai

servers:
  - url: https://api.agnost.ai
    description: Production

tags:
  - name: SDK
    description: Event ingestion endpoints used by SDKs
  - name: Dashboard
    description: Analytics and metrics for the dashboard
  - name: Organizations
    description: Organization management
  - name: Alerts
    description: Alert configuration and monitoring
  - name: SOPs
    description: Standard Operating Procedures
  - name: Sentiments
    description: Sentiment and intent analysis
  - name: Conversations
    description: Conversation message and span retrieval
  - name: Classification
    description: LLM-based classification operations
  - name: Onboarding
    description: First-run helpers used during organization setup
  - name: Connections
    description: External integrations (Slack)
  - name: Billing
    description: Stripe billing management
  - name: API Keys
    description: API key management
  - name: Auth
    description: OAuth authentication callbacks
  - name: System
    description: Health checks, webhooks, internal endpoints

components:
  securitySchemes:
    OrgId:
      type: apiKey
      in: header
      name: x-org-id
      description: "Organization ID (UUID). Used by SDKs for event ingestion. Header is case-insensitive."
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token from OAuth login.
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: API key (`agnost_<64-hex>`) for programmatic dashboard access. Issued via Settings → API Keys.
    OrgScope:
      type: apiKey
      in: header
      name: x-org-id
      description: Optional. Selects which organization a JWT- or API-key-authenticated request targets when the credential has access to multiple.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required: [error]
      example:
        error: "Invalid request body"

    Connection:
      type: object
      required: [id, org_id, connection_type, connection_name, created_at, created_by, updated_at, active]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        connection_type:
          type: string
          enum: [slack]
        connection_name:
          type: string
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
          format: uuid
        updated_at:
          type: string
          format: date-time
        active:
          type: integer
          enum: [0, 1]
        team_name:
          type: string
        channel_name:
          type: string
        status:
          type: string
          enum: [ready, needs_invite, degraded, unknown]
        manual_invite_required:
          type: boolean
        last_error:
          type: string
        webhook_ready:
          type: boolean
        bot_member:
          type: boolean
        mentions_ready:
          type: boolean
        delivery_healthy:
          type: boolean

    TimeRange:
      type: string
      enum: ["1min", "1h", "1d", "1w", "1m", "all"]
      default: "1w"

    QueryFilters:
      type: object
      properties:
        time_range:
          $ref: '#/components/schemas/TimeRange'
        connection_type:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        agent_name:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        metadata_filters:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              value:
                type: string
        page:
          type: integer
          default: 1
        page_size:
          type: integer
          default: 20

    RawLogsQueryFilters:
      allOf:
        - $ref: '#/components/schemas/QueryFilters'
        - type: object
          properties:
            event_id:
              type: string
              format: uuid
              description: Fetches one exact event without applying the approximate raw-log time-window narrowing.
            conversation_id:
              type: string
              format: uuid
              description: Filters events to one conversation, independently of event_id, and uses the event table's primary-key prefix for an efficient lookup.
            search_query:
              type: string
            success:
              type: boolean
            exclude_io:
              type: boolean
              default: false
              description: When false, each event includes input/output previews capped at 64 KiB per field. Use `/dashboard/api/event-io` for the full payload of one event.

    UserStoriesQueryFilters:
      allOf:
        - $ref: '#/components/schemas/QueryFilters'
        - type: object
          properties:
            search:
              type: string
              description: Case-insensitive identifier search used to load matching search options.
            search_field:
              type: string
              enum: [user_id, conversation_id, original_conversation_id, user_email, group_value]
              description: Structured identifier selected from User Stories search. Grouped requests accept only `group_value`.
            search_value:
              type: string
              description: Exact value for the selected search field.
            group_by:
              $ref: '#/components/schemas/UserStoriesGroupBy'
            sort:
              type: string
              enum: [newest, oldest]
              default: newest
            exclude_io:
              type: boolean
              default: false
              description: Omits event input/output payloads when true.
            allow_limited_preview:
              type: boolean
              default: false
              description: Dashboard-only flag for a small access-limited preview; programmatic clients should leave false.
            sentiment_ids:
              type: array
              items: { type: string, format: uuid }
              description: Restricts conversations to events matching any selected intent id.
            sop_ids:
              type: array
              items: { type: string, format: uuid }
              description: Restricts conversations to events matching any selected violation id.

    UserStoriesListFilters:
      allOf:
        - $ref: '#/components/schemas/UserStoriesQueryFilters'
        - type: object
          properties:
            page_size:
              type: integer
              minimum: 1
              maximum: 100
              default: 50
              description: Conversations, groups, or users per page. The dashboard requests 15 distinct group values at a time.
            group_level:
              type: string
              enum: [groups, users, conversations]
              description: When `group_by` is present, returns metadata groups by default, users within `group_value`, or conversations for `group_user_key`.
            group_value:
              type: string
              description: Required when `group_level` is `users` or `conversations`.
            group_user_key:
              type: string
              description: "Opaque user key returned by grouped users; required for `group_level: conversations`."

    UserStoriesGroupBy:
      type: object
      required: [source, key]
      properties:
        source:
          type: string
          enum: [conversation_metadata, user_metadata]
        key:
          type: string
          description: Metadata-map key to group by.

    UserStoriesCostMetadata:
      type: object
      additionalProperties:
        type: string
      description: >-
        Pricing inputs aggregated from eligible events. Entries contain an explicit
        cost or a model/provider and token totals; `tokens.aggregated` is `true` for
        summary buckets, while `tokens.request_input` preserves the per-request input
        size used to select a model-pricing tier. `pricing.invalid` is `true` when
        malformed numeric metadata makes that bucket unpriceable.

    UserStoriesGroupSummary:
      type: object
      required: [value, conversation_count, user_count, message_count, intent_count, intent_labels, violation_count, violation_labels, last_active]
      properties:
        value: { type: string }
        conversation_count: { type: integer, minimum: 0 }
        user_count: { type: integer, minimum: 0 }
        message_count:
          type: integer
          minimum: 0
          description: Distinct captured events across the group's eligible conversations, including tool events.
        intent_count: { type: integer, minimum: 0 }
        intent_labels:
          type: array
          items: { type: string }
        violation_count: { type: integer, minimum: 0 }
        violation_labels:
          type: array
          items: { type: string }
        last_active: { type: string, format: date-time }
        cost_metadata:
          type: array
          items: { $ref: '#/components/schemas/UserStoriesCostMetadata' }

    UserStoriesUserSummary:
      type: object
      required: [key, label, representative_conversation_id, conversation_count, message_count, intent_count, intent_labels, violation_count, violation_labels, last_active]
      properties:
        key: { type: string }
        user_id: { type: string }
        label: { type: string }
        representative_conversation_id: { type: string }
        conversation_count: { type: integer, minimum: 0 }
        message_count:
          type: integer
          minimum: 0
          description: Distinct captured events across the user's eligible conversations, including tool events.
        intent_count: { type: integer, minimum: 0 }
        intent_labels:
          type: array
          items: { type: string }
        violation_count: { type: integer, minimum: 0 }
        violation_labels:
          type: array
          items: { type: string }
        last_active: { type: string, format: date-time }
        cost_metadata:
          type: array
          items: { $ref: '#/components/schemas/UserStoriesCostMetadata' }

    UserStoriesVisibleUserSummary:
      type: object
      required: [key, conversation_count, message_count, intent_count, intent_labels, violation_count, violation_labels]
      properties:
        key:
          type: string
          description: Stable `user:<user_id>` key, or `conversation:<conversation_id>` for an anonymous conversation.
        conversation_count: { type: integer, minimum: 0 }
        message_count:
          type: integer
          minimum: 0
          description: Distinct captured events across the user's eligible conversations, including tool events.
        intent_count: { type: integer, minimum: 0 }
        intent_labels:
          type: array
          items: { type: string }
        violation_count: { type: integer, minimum: 0 }
        violation_labels:
          type: array
          items: { type: string }
        cost_metadata:
          type: array
          items: { $ref: '#/components/schemas/UserStoriesCostMetadata' }

    UserStoriesResponse:
      type: object
      description: Returns `conversations`, `groups`, or `users` according to the grouping request.
      properties:
        conversations:
          type: array
          items: { type: object, additionalProperties: true }
        groups:
          type: array
          items: { $ref: '#/components/schemas/UserStoriesGroupSummary' }
        users:
          type: array
          items: { $ref: '#/components/schemas/UserStoriesUserSummary' }
        user_summaries:
          type: array
          description: Full filtered-history metrics for users on the current ungrouped interactive page. Empty for grouped or limited-preview responses. Metrics degrade to zero values when optional aggregation is unavailable.
          items: { $ref: '#/components/schemas/UserStoriesVisibleUserSummary' }
        group_value: { type: string }
        overlapping:
          type: boolean
          description: Metadata arrays may place a conversation in more than one group.
        page: { type: integer, minimum: 1 }
        page_size: { type: integer, minimum: 1 }
        total_pages: { type: integer, minimum: 0 }
        has_more:
          type: boolean
          description: True when one lookahead row proves that another page is available.
        partial_data:
          type: boolean
          description: True when optional hydration or enrichment failed and the returned page is incomplete.

    DashboardTimelinePoint:
      type: object
      required: [day, count]
      properties:
        day:
          type: string
          format: date-time
        count:
          type: integer
          minimum: 0

    CommandCenterMetrics:
      type: object
      required:
        - total_calls
        - total_conversations
        - active_users
        - daily_calls
        - daily_users
        - daily_conversations
      properties:
        total_calls:
          type: integer
          minimum: 0
        total_conversations:
          type: integer
          minimum: 0
          description: Exact distinct conversation count in the selected period.
        active_users:
          type: integer
          minimum: 0
        daily_calls:
          type: array
          items:
            $ref: '#/components/schemas/DashboardTimelinePoint'
        daily_users:
          type: array
          items:
            $ref: '#/components/schemas/DashboardTimelinePoint'
        daily_conversations:
          type: array
          items:
            $ref: '#/components/schemas/DashboardTimelinePoint'

    AnalyticsTagCount:
      type: object
      required: [tag, count]
      properties:
        tag:
          type: string
        count:
          type: integer
          minimum: 0

    AnalyticsTagStats:
      type: object
      required: [total_analyzed, total_messages, top_tags, tag_distribution]
      properties:
        total_analyzed:
          type: integer
          minimum: 0
        total_messages:
          type: integer
          minimum: 0
        top_tags:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/AnalyticsTagCount'
        tag_distribution:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/AnalyticsTagCount'

    ClassificationRequest:
      type: object
      properties:
        time_range:
          $ref: '#/components/schemas/TimeRange'
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 50
          description: Maximum conversations to classify in this request
        offset:
          type: integer
          minimum: 0
          default: 0
        force:
          type: boolean
          default: false
          description: Reclassify conversations already marked as analyzed
        run_id:
          type: string
          description: Client-generated identifier used to cancel a multi-batch run

    AlertConfig:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        event_type:
          type: string
          enum: [custom, global_report, suggested_intent]
          description: "suggested_intent alerts are triggered by intent discovery rather than the scheduled alert poller."
        natural_language_query:
          type: string
        sql_query:
          type: string
        context_sql_query:
          type: string
          description: Required for custom alerts. Returns up to five recent detail rows after the condition threshold is met.
        action_natural_language_query:
          type: string
          description: Optional instruction used only when no details query is available.
        threshold_value:
          type: number
        threshold_operator:
          type: string
          enum: [">", "<", ">=", "<=", "=="]
        check_interval_hours:
          type: integer
        connection_ids:
          type: array
          items:
            type: string
            format: uuid
        recipient_emails:
          type: array
          items:
            type: string
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AlertConfigInput:
      allOf:
        - $ref: '#/components/schemas/AlertConfig'
        - type: object
          properties:
            event_type:
              type: string
              enum: [custom, global_report]

    SOPConfig:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
        active:
          type: boolean

    SentimentConfig:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
        active:
          type: boolean

    Organization:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        subscription:
          type: string
        created_at:
          type: string
          format: date-time

    APIKeyRecord:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        created_by:
          type: string

  responses:
    BadRequest:
      description: Invalid request body
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: No access to the requested organization
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

paths:
  /healthz:
    get:
      tags: [System]
      summary: Health check
      operationId: healthCheck
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            text/plain:
              schema:
                type: string
                example: ok

  /api/v1/capture-session:
    post:
      tags: [SDK]
      summary: Capture session
      description: Start a session. Call once per conversation; reuse `session_id` on every event.
      operationId: captureSession
      parameters:
        - name: x-org-id
          in: header
          required: true
          description: Your organization ID (UUID, case-insensitive).
          schema:
            type: string
            example: "<org-id>"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, user_data]
              properties:
                session_id:
                  type: string
                  format: uuid
                  description: Session UUID. Reuse on every event in this conversation.
                  example: a3f9c182-7d4e-4b6a-9e21-c5d8f0b4e731
                user_data:
                  type: object
                  description: End-user identity. `user_id` is required; other traits are supported for analytics. The example shows shape, but production integrations should prefer a stable pseudonymous ID and avoid raw email, name, phone, or other personal data unless explicitly approved.
                  required: [user_id]
                  properties:
                    user_id:
                      type: string
                      example: user-anon-002
                  additionalProperties:
                    type: string
                  example:
                    user_id: user-anon-002
                    email: user@example.com
                    user_plan: pro
                metadata:
                  type: object
                  additionalProperties:
                    type: string
                  description: Free-form session metadata. Prefer allowlisted operational fields; do not send secrets or sensitive personal data.
                  example:
                    language: hi-IN
                timestamp:
                  type: integer
                  format: int64
                  description: Unix time in **ms** when the session began. Defaults to server time.
                  example: 1714867200000
                client_config:
                  type: string
                  description: Free-form client/SDK label.
                  example: client-segment-001
      responses:
        "200":
          description: Session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id:
                    type: string
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/v1/capture-event:
    post:
      tags: [SDK]
      summary: Capture event
      description: Record one turn-pair or tool call. Generate `event_id` client-side so children can reference parents.
      operationId: captureEvent
      parameters:
        - name: x-org-id
          in: header
          required: true
          description: Your organization ID (UUID, case-insensitive).
          schema:
            type: string
            example: "<org-id>"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_id, session_id, primitive_name, args, result]
              properties:
                event_id:
                  type: string
                  format: uuid
                  description: Client-generated UUID. Use as `parent_id` on child events.
                  example: e8b1c52f-3a9d-4e7c-8f0b-2d6a91c4ef58
                session_id:
                  type: string
                  format: uuid
                  description: Session UUID from `capture-session`.
                  example: a3f9c182-7d4e-4b6a-9e21-c5d8f0b4e731
                primitive_name:
                  type: string
                  description: Agent name (turn-pair) or tool name (tool call).
                  example: your-agent-name
                args:
                  type: string
                  description: Input — user message or JSON-encoded tool args.
                  example: "your input in plain text or JSON-encoded string"
                result:
                  type: string
                  description: Output — assistant reply or JSON-encoded tool result.
                  example: "your output in plain text or JSON-encoded string"
                success:
                  type: boolean
                  description: Defaults to `true`.
                latency:
                  type: integer
                  description: Execution time in **ms**.
                  example: 5200
                timestamp:
                  type: integer
                  format: int64
                  description: Unix time in **ms** when the event occurred. Defaults to server time.
                  example: 1714867201000
                parent_id:
                  type: string
                  format: uuid
                  description: Parent event UUID. Set on tool calls to point at the agent turn (or parent tool) that triggered them.
                  example: 4c7d2e8a-1b95-4f3d-a08e-7b3c9d12f5e6
                metadata:
                  type: object
                  additionalProperties:
                    type: string
                  description: Free-form event metadata.
                  example:
                    language: hi-IN
      responses:
        "200":
          description: Event recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  event_id:
                    type: string
                    format: uuid
        "400":
          $ref: '#/components/responses/BadRequest'

  /api/voice/v1/webhooks/retell:
    post:
      tags: [SDK]
      summary: Ingest an analyzed Retell call
      description: |
        Translates a Retell `call_analyzed` webhook into one Agnost voice
        conversation. Other Retell event types are acknowledged without capture.
        Word timestamps, post-call analysis, call latency, cost, and tool calls
        are retained when present. Retell's call access token is never stored.
      operationId: ingestRetellAnalyzedCall
      parameters:
        - name: org_id
          in: query
          required: true
          description: Your Agnost organization ID.
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, call]
              properties:
                event:
                  type: string
                  example: call_analyzed
                event_timestamp:
                  type: integer
                  format: int64
                call:
                  type: object
                  required: [call_id]
                  additionalProperties: true
                  properties:
                    call_id:
                      type: string
                    call_type:
                      type: string
                    agent_id:
                      type: string
                    agent_name:
                      type: string
                    start_timestamp:
                      type: integer
                      format: int64
                    end_timestamp:
                      type: integer
                      format: int64
                    duration_ms:
                      type: number
                    transcript:
                      type: string
                    transcript_object:
                      type: array
                      items:
                        type: object
                        additionalProperties: true
                    transcript_with_tool_calls:
                      type: array
                      items:
                        type: object
                        additionalProperties: true
                    call_analysis:
                      type: object
                      additionalProperties: true
                    call_cost:
                      type: object
                      additionalProperties: true
              additionalProperties: true
      responses:
        "200":
          description: Analyzed call accepted and translated into a conversation.
          content:
            application/json:
              schema:
                type: object
                required: [status, session_id, user_id, event_count]
                properties:
                  status:
                    type: string
                    enum: [accepted]
                  session_id:
                    type: string
                  user_id:
                    type: string
                  event_count:
                    type: integer
        "204":
          description: Event acknowledged without ingestion.
        "400":
          description: Invalid organization ID or malformed Retell envelope.
        "413":
          description: Transcript exceeds the one-megabyte ingestion limit.
        "422":
          description: Retell data could not be normalized.
        "502":
          description: Downstream Agnost ingestion failed.

  /api/voice/v1/webhooks/vapi:
    post:
      tags: [SDK]
      summary: Ingest a Vapi end-of-call report
      description: |
        Translates a Vapi `end-of-call-report` into one Agnost voice
        conversation. The Vapi call ID becomes the conversation ID, the caller
        number becomes the user ID, and each caller/assistant exchange becomes
        an event. The completed call total is attached to exactly one event so
        conversation cost aggregation does not multiply it by the turn count.

        The caller number may be supplied in either `message.call.customer.number`
        or `message.customer.number`. Transcript content may be supplied in
        `message.artifact.messages`, `message.artifact.transcript`, or
        `message.transcript`. Internal function calls found in the artifact are
        stored as child tool events beneath their synthetic agent tool-call
        events, including their arguments, results, timing, and success state.
      operationId: ingestVapiEndOfCallReport
      parameters:
        - name: x-org-id
          in: header
          required: true
          description: Your Agnost organization ID (UUID, case-insensitive).
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: object
                  required: [type, call]
                  properties:
                    type:
                      type: string
                      enum: [end-of-call-report]
                    startedAt:
                      type: string
                      format: date-time
                    endedAt:
                      type: string
                      format: date-time
                    endedReason:
                      type: string
                    durationSeconds:
                      type: number
                    cost:
                      type: number
                      minimum: 0
                      description: Vapi's completed total call cost in USD.
                    transcript:
                      type: string
                    customer:
                      type: object
                      properties:
                        number:
                          type: string
                    analysis:
                      type: object
                      additionalProperties: true
                    artifact:
                      type: object
                      additionalProperties: true
                      properties:
                        transcript:
                          type: string
                        messages:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                        recordingUrl:
                          type: string
                          format: uri
                        stereoRecordingUrl:
                          type: string
                          format: uri
                        logUrl:
                          type: string
                          format: uri
                    call:
                      type: object
                      required: [id]
                      additionalProperties: true
                      properties:
                        id:
                          type: string
                        orgId:
                          type: string
                        assistantId:
                          type: string
                        phoneNumberId:
                          type: string
                        type:
                          type: string
                        status:
                          type: string
                        customer:
                          type: object
                          properties:
                            number:
                              type: string
                        assistant:
                          type: object
                          properties:
                            id:
                              type: string
                            name:
                              type: string
                  additionalProperties: true
              additionalProperties: true
      responses:
        "200":
          description: Report accepted and translated into a conversation.
          content:
            application/json:
              schema:
                type: object
                required: [status, session_id, user_id, event_count]
                properties:
                  status:
                    type: string
                    enum: [accepted]
                  session_id:
                    type: string
                  user_id:
                    type: string
                  event_count:
                    type: integer
        "400":
          description: Invalid organization ID, event type, call ID, or caller number.
        "413":
          description: Transcript exceeds the one-megabyte ingestion limit.
        "422":
          description: Missing organization header or usable transcript.
        "500":
          description: Downstream Agnost ingestion failed.

  /dashboard/api/profile:
    get:
      tags: [Organizations]
      summary: Get current user profile
      operationId: getProfile
      security:
        - BearerAuth: []
          OrgScope: []
      responses:
        "200":
          description: User profile with org info
        "401":
          $ref: '#/components/responses/Unauthorized'

  /dashboard/api/organizations:
    get:
      tags: [Organizations]
      summary: List organizations
      operationId: listOrganizations
      security:
        - BearerAuth: []
      responses:
        "200":
          description: List of organizations the user belongs to
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Organization'
    post:
      tags: [Organizations]
      summary: Create organization
      operationId: createOrganization
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "201":
          description: Organization created
        "400":
          $ref: '#/components/responses/BadRequest'

  /dashboard/api/organizations/{id}:
    put:
      tags: [Organizations]
      summary: Update organization
      operationId: updateOrganization
      security:
        - BearerAuth: []
          OrgScope: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Organization updated
    delete:
      tags: [Organizations]
      summary: Delete organization
      operationId: deleteOrganization
      security:
        - BearerAuth: []
          OrgScope: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Organization deleted

  /dashboard/api/organization/displayId:
    get:
      tags: [Organizations]
      summary: Get the public-facing org display ID
      description: |
        Returns the short, public-facing display ID for the active organization.
        Used by the dashboard UI to render a user-friendly org identifier
        without exposing the internal UUID.
      operationId: getOrgDisplayID
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: Display ID for the active organization
          content:
            application/json:
              schema:
                type: object
                properties:
                  display_id:
                    type: string

  /dashboard/api/organization/summary:
    get:
      tags: [Organizations]
      summary: Get organization summary
      description: Returns the selected organization's identity and whether its event aggregate contains any data.
      operationId: getOrganizationSummary
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: Organization identity and data-presence flag
          content:
            application/json:
              schema:
                type: object
                required: [id, name, has_data]
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  has_data:
                    type: boolean
                    description: True when the organization has at least one row in event_daily_stats.
        "404":
          description: Organization not found

  /dashboard/api/organizations/{id}/members:
    get:
      tags: [Organizations]
      summary: List organization members
      operationId: listOrgMembers
      security:
        - BearerAuth: []
          OrgScope: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: List of members
    post:
      tags: [Organizations]
      summary: Add member to organization
      operationId: addOrgMember
      security:
        - BearerAuth: []
          OrgScope: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Member added

  /dashboard/api/login-summary:
    get:
      tags: [Dashboard]
      summary: Generate login summary
      description: |
        Generates a fresh 48-hour login summary for the selected organization.
        The response is not persisted; dashboard clients request it after org
        selection settles, then keep it in memory for the current page session.
        If no recent data exists, the endpoint returns a generic `summary_text`
        with an empty `sections` array.
      operationId: getLoginSummary
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: org_id
          in: query
          required: false
          description: Organization ID. Optional when org scope is supplied by JWT/API-key auth or `x-org-id`.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Generated login summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  org_id:
                    type: string
                    format: uuid
                  window_start:
                    type: string
                    format: date-time
                  window_end:
                    type: string
                    format: date-time
                  generated_at:
                    type: string
                    format: date-time
                  summary_text:
                    type: string
                    description: LLM-written summary capped at 90 words, or a generic no-data fallback.
                  sections:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                          example: errors
                        title:
                          type: string
                          example: Critical errors
                        href:
                          type: string
                          example: /errors
                        items:
                          type: array
                          items:
                            type: object
                            properties:
                              title:
                                type: string
                              detail:
                                type: string
                              count:
                                type: integer
                              href:
                                type: string
              example:
                id: 89d7ad7e-0248-4f98-abfa-1278ce5b7b4a
                org_id: 5344ec46-cfc4-478a-805f-8c9d0b5ed66a
                window_start: "2026-07-06T12:00:00Z"
                window_end: "2026-07-08T12:00:00Z"
                generated_at: "2026-07-08T12:00:00Z"
                summary_text: "Critical errors remained concentrated in policy checks and webhook probes, with feature-request and billing-dispute intents leading the last 48 hours."
                sections:
                  - key: errors
                    title: Critical errors
                    href: /errors
                    items:
                      - title: policy_check
                        detail: 4 critical errors in the last 48 hours
                        count: 4
                        href: /errors
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "500":
          $ref: '#/components/responses/ServerError'

  /dashboard/api/user-stories:
    post:
      tags: [Dashboard]
      summary: Get user stories (conversations)
      operationId: getUserStories
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserStoriesListFilters'
      responses:
        "200":
          description: Recent conversations, metadata groups, or users within a metadata group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserStoriesResponse'

  /dashboard/api/user-stories/search-options:
    post:
      tags: [Dashboard]
      summary: Search User Stories identifiers or grouped values
      description: Searches User ID, conversation ID, original conversation ID, and user email when ungrouped. When `group_by` is present, searches only values from that selected metadata field.
      operationId: searchUserStoryIdentifiers
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserStoriesQueryFilters'
      responses:
        "200":
          description: Matching structured identifiers from conversations in the selected organization and time range
          content:
            application/json:
              schema:
                type: object
                required: [options]
                properties:
                  options:
                    type: array
                    items:
                      type: object
                      required: [field, label, value]
                      properties:
                        field:
                          type: string
                          enum: [user_id, conversation_id, original_conversation_id, user_email, group_value]
                        label:
                          type: string
                        value:
                          type: string

  /dashboard/api/tool-stats:
    post:
      tags: [Dashboard]
      summary: Get tool usage statistics
      operationId: getToolStats
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Tool usage breakdown

  /dashboard/api/tool-checkpoint-stats:
    post:
      tags: [Dashboard]
      summary: Get tool checkpoint timing stats
      operationId: getToolCheckpointStats
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Checkpoint timing statistics

  /dashboard/api/tool-error-distribution:
    post:
      tags: [Dashboard]
      summary: Get error distribution by tool
      operationId: getToolErrorDistribution
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Error distribution for the newest 1,000 critical error events matching the tool
          content:
            application/json:
              schema:
                type: object
                required: [error_messages, total_errors_in_range, sample_size, is_truncated]
                properties:
                  error_messages:
                    type: array
                    nullable: true
                    maxItems: 10
                    items:
                      type: object
                      required: [message, count]
                      properties:
                        message:
                          type: string
                        count:
                          type: integer
                          minimum: 1
                  total_errors_in_range:
                    type: integer
                    minimum: 0
                    maximum: 1000
                    description: Number of errors represented by this newest-error sample
                  sample_size:
                    type: integer
                    minimum: 0
                    maximum: 1000
                  is_truncated:
                    type: boolean
                    description: True when additional matching errors exist beyond the returned sample

  /dashboard/api/errors:
    post:
      tags: [Dashboard]
      summary: Get error events
      operationId: getErrors
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  properties:
                    page_size:
                      type: integer
                      minimum: 1
                      maximum: 1000
                      default: 1000
                    severity:
                      type: string
                      enum: [all, critical, non_critical]
      responses:
        "200":
          description: Error statistics and up to the newest 1,000 matching events; Critical uses error_events, Non-critical uses filtered event rows, and All merges both
          content:
            application/json:
              schema:
                type: object
                properties:
                  recent_errors:
                    type: array
                    items:
                      type: object
                      properties:
                        event_id:
                          type: string
                        event_type:
                          type: string
                        event_name:
                          type: string
                        timestamp:
                          type: string
                          format: date-time
                        latency:
                          type: integer
                        error_message:
                          type: string
                  recent_errors_truncated:
                    type: boolean
                    description: True only when more than 1,000 events match the request filters

  /dashboard/api/errors/export:
    post:
      tags: [Dashboard]
      summary: Export errors as CSV
      operationId: exportErrors
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: CSV containing the newest 10,000 errors matching the selected range and filters
          headers:
            X-Export-Row-Limit:
              description: Maximum number of matching rows included in the export
              schema:
                type: integer
                example: 10000
          content:
            text/csv:
              schema:
                type: string

  /dashboard/api/raw-logs:
    post:
      tags: [Dashboard]
      summary: Get raw event logs
      description: Returns a paginated event list. Input/output hydration runs only for the selected page and each field is capped at a 64 KiB preview; use `/dashboard/api/event-io` for a full single-event payload.
      operationId: getRawLogs
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RawLogsQueryFilters'
      responses:
        "200":
          description: Paginated raw log list

  /dashboard/api/raw-logs/export:
    post:
      tags: [Dashboard]
      summary: Export raw logs as CSV
      operationId: exportRawLogs
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: CSV file download
          content:
            text/csv:
              schema:
                type: string

  /dashboard/api/raw-logs/count:
    post:
      tags: [Dashboard]
      summary: Get total count for raw logs query
      description: |
        Returns the total event count matching the MV-compatible subset of
        `/raw-logs` filters, served from the `event_daily_stats` materialized
        view for sub-second latency. Exact `event_id` and `conversation_id`
        filters are not supported. The frontend issues this in parallel with
        `/raw-logs` so the page renders immediately and the total fills in
        when ready.

        Returns `{"total_count": null}` when the filter set cannot be answered
        by the materialized view (search query, client_config, connection_type,
        or arbitrary metadata keys other than event_type).
      operationId: getRawLogsCount
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Total count, or null if filters are MV-incompatible
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_count:
                    type: integer
                    nullable: true

  /dashboard/api/event-io:
    post:
      tags: [Dashboard]
      summary: Get input/output for a specific event
      operationId: getEventIO
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [event_id]
              properties:
                event_id:
                  type: string
                  format: uuid
                conversation_id:
                  type: string
                  format: uuid
                  description: Optional conversation key for an efficient primary-key lookup.
      responses:
        "200":
          description: Event input and output data

  /dashboard/api/metadata-distribution:
    post:
      tags: [Dashboard]
      summary: Get metadata key distribution
      operationId: getMetadataDistribution
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [metadata_source, metadata_field]
                  properties:
                    metadata_source:
                      type: string
                      enum: [event_metadata, event_field, conversation_metadata, user_metadata, user_id, conversation_user_id]
                      description: Use conversation_user_id to rank stable conversation user IDs without merging display names or emails.
                    metadata_field:
                      type: string
      responses:
        "200":
          description: Distribution of metadata keys

  /dashboard/api/command-center-summary:
    post:
      tags: [Dashboard]
      summary: Get combined command center data
      description: >-
        Returns the fixed stats-only Command Center summary: event, user, and
        conversation totals and timeline buckets plus top error aggregates.
        Eligible multi-day unfiltered event windows use daily materialized views;
        short or filtered windows retain raw filtering semantics. SOP stats,
        sentiment stats, and the separate 120-event raw-log feed load independently.
      operationId: getCommandCenterSummary
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  properties:
                    offset_hours:
                      type: integer
                      minimum: 0
                      maximum: 720
                      default: 0
                      description: Shifts the selected window into the past by this many hours.
      responses:
        "200":
          description: Combined Command Center metrics and errors
          content:
            application/json:
              schema:
                type: object
                required: [metrics, errors]
                properties:
                  metrics:
                    nullable: true
                    allOf:
                      - $ref: '#/components/schemas/CommandCenterMetrics'
                  errors:
                    type: object
                    nullable: true

  /dashboard/api/tools-dashboard:
    post:
      tags: [Dashboard]
      summary: Get combined tools dashboard data
      description: Combines tool stats, time series, and saved charts
      operationId: getToolsDashboard
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Combined tools data

  /dashboard/api/conversation-messages:
    post:
      tags: [Conversations]
      summary: Get conversation messages
      operationId: getConversationMessages
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [conversation_id]
              properties:
                conversation_id:
                  type: string
                  format: uuid
                time_range:
                  type: string
                  description: Optional reporting window applied to conversation events.
                page:
                  type: integer
                  minimum: 1
                  default: 1
                limit:
                  type: integer
                  minimum: 1
                  description: Event rows per page; explicitly supplied values are capped at 100.
                load_io:
                  type: boolean
                  default: true
                  description: Resolves readable input/output message content when true.
      responses:
        "200":
          description: Conversation messages, with multimodal text/image_url content encoded in versioned message strings
          content:
            application/json:
              schema:
                type: object
                required: [conversation_id, timestamp, user_id, messages, total_messages, has_more]
                properties:
                  conversation_id:
                    type: string
                    format: uuid
                  timestamp:
                    type: string
                    format: date-time
                  user_id:
                    type: string
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        role: { type: string }
                        timestamp: { type: string, format: date-time }
                        latency: { type: integer }
                        success: { type: boolean }
                        message: { type: string }
                  total_messages:
                    type: integer
                    description: Number of rendered messages in this page.
                  has_more:
                    type: boolean
                    description: True when another event page is available for the same query.

  /dashboard/api/conversation-spans:
    post:
      tags: [Conversations]
      summary: Get conversation spans (trace)
      operationId: getConversationSpans
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [conversation_id]
              properties:
                conversation_id:
                  type: string
                  format: uuid
                event_id:
                  type: string
                  format: uuid
                  description: "Optional. Filter to ancestors/descendants of this event"
      responses:
        "200":
          description: Span hierarchy for the conversation

  /dashboard/api/conversation-audio:
    get:
      tags: [Conversations]
      summary: Get a presigned URL for a conversation's recorded audio
      description: |
        Returns a short-lived presigned URL the dashboard uses to play back
        recorded audio for a voice conversation. The audio is stored in
        object storage and only addressable through this endpoint.
      operationId: getConversationAudio
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: conversation_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Presigned audio URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                  expires_at:
                    type: string
                    format: date-time
        "404":
          description: No audio recorded for this conversation

  /dashboard/api/conversation-detail:
    post:
      tags: [Conversations]
      summary: Get conversation detail (messages + spans combined)
      operationId: getConversationDetail
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [conversation_id]
              properties:
                conversation_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Combined messages and spans
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: object
                  spans:
                    type: object

  /dashboard/api/spotlight-search:
    post:
      tags: [Conversations]
      summary: Text search across conversations
      operationId: spotlightSearch
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  properties:
                    question:
                      type: string
                      description: Natural-language analytics question. Preferred over `query` when both are supplied.
                    query:
                      type: string
                      description: Backward-compatible natural-language analytics question.
                    response_format:
                      type: string
                      enum: [json]
                      description: Optional structured-answer mode. Requires `response_schema`.
                    response_schema:
                      type: object
                      maxProperties: 100
                      additionalProperties: true
                      description: Bounded JSON response template whose keys and value types the final Spotlight answer must match.
                  anyOf:
                    - required: [question]
                    - required: [query]
      responses:
        "200":
          description: Search results

  /dashboard/api/sops:
    get:
      tags: [SOPs]
      summary: List SOPs
      operationId: listSOPs
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: List of SOPs
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SOPConfig'
    post:
      tags: [SOPs]
      summary: Create SOP
      description: Active SOPs are applied to new messages and asynchronously evaluated against up to 1,000 recent conversations.
      operationId: createSOP
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [title, description]
              properties:
                title:
                  type: string
                description:
                  type: string
                active:
                  type: boolean
      responses:
        "201":
          description: SOP created

  /dashboard/api/sops/{id}:
    put:
      tags: [SOPs]
      summary: Update SOP
      description: Activating an SOP or changing an active SOP's description asynchronously re-evaluates up to 1,000 recent conversations.
      operationId: updateSOP
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                active:
                  type: boolean
      responses:
        "200":
          description: SOP updated
    delete:
      tags: [SOPs]
      summary: Delete SOP
      operationId: deleteSOP
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: SOP deleted

  /dashboard/api/sop-violations:
    post:
      tags: [SOPs]
      summary: Get SOP violation events
      operationId: getSOPViolations
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Events that violated SOPs

  /dashboard/api/sop-stats:
    post:
      tags: [SOPs]
      summary: Get SOP violation distribution stats
      description: |
        Mirrors `/sentiment-stats`. Returns total analyzed events, total events
        in the time range, top violating SOPs, and the full SOP distribution.
      operationId: getSOPStats
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: SOP distribution with counts per SOP title
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsTagStats'

  /dashboard/api/sop-messages:
    post:
      tags: [SOPs]
      summary: Get assistant messages matching a specific SOP violation
      description: |
        Mirrors `/sentiment-messages`, but matches on the assistant output rather
        than the user input. The `tag` field is the SOP title to filter on.
      operationId: getMessagesBySOP
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [tag]
                  properties:
                    tag:
                      type: string
                      description: SOP title to match
                    page:
                      type: integer
                      default: 1
                    limit:
                      type: integer
                      default: 50
      responses:
        "200":
          description: Paginated assistant messages that violated the SOP

  /dashboard/api/sop-timeline:
    post:
      tags: [SOPs]
      summary: Get timeline for a single SOP violation
      description: Mirrors `/sentiment-timeline`. Bucketing is automatic based on the time range.
      operationId: getSOPTimeline
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [tag]
                  properties:
                    tag:
                      type: string
                      description: SOP title
      responses:
        "200":
          description: Per-bucket violation counts plus the bucket size in seconds

  /dashboard/api/sop-timelines:
    post:
      tags: [SOPs]
      summary: Get per-SOP timelines for multiple SOP titles in a single call
      description: Mirrors `/sentiment-timelines`. Pass an array of SOP titles in `tags`.
      operationId: getBatchSOPTimelines
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [tags]
                  properties:
                    tags:
                      type: array
                      items:
                        type: string
      responses:
        "200":
          description: Map of SOP title → timeline data points

  /dashboard/api/summarize-sop-distribution:
    post:
      tags: [SOPs]
      summary: LLM summary of why a SOP is being violated
      description: |
        Pulls assistant outputs that violated the given SOP within the time
        range and produces a natural-language summary via the LLM. Mirrors
        `/summarize-intent-distribution`.
      operationId: summarizeSOPDistribution
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                org_id:
                  type: string
                  format: uuid
                tag:
                  type: string
                  description: SOP title to summarize
                time_range:
                  $ref: '#/components/schemas/TimeRange'
      responses:
        "200":
          description: Natural-language summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: string

  /dashboard/api/sentiments:
    get:
      tags: [Sentiments]
      summary: List sentiment configs
      operationId: listSentiments
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: List of sentiment configurations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SentimentConfig'
    post:
      tags: [Sentiments]
      summary: Create sentiment (intent) config
      operationId: createSentiment
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [title, description]
              properties:
                title:
                  type: string
                description:
                  type: string
                active:
                  type: boolean
      responses:
        "201":
          description: Sentiment created

  /dashboard/api/enrich-intent:
    post:
      tags: [Sentiments]
      summary: Enrich one intent description
      description: |
        Improves an existing User Intent description while preserving its name.
        The returned description contains explicit Includes and Excludes clauses.
      operationId: enrichIntent
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, description]
              additionalProperties: false
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                description:
                  type: string
                  maxLength: 2000
      responses:
        "200":
          description: Enriched intent description
          content:
            application/json:
              schema:
                type: object
                required: [description]
                additionalProperties: false
                properties:
                  description:
                    type: string
                    maxLength: 2000
        "400":
          description: Invalid request body or missing organization scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "422":
          description: Invalid intent name or description
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "500":
          description: Intent enrichment failed or returned invalid output
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        "503":
          description: AI service is not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /dashboard/api/sentiments/{id}:
    put:
      tags: [Sentiments]
      summary: Update sentiment config
      operationId: updateSentiment
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                active:
                  type: boolean
      responses:
        "200":
          description: Sentiment updated
    delete:
      tags: [Sentiments]
      summary: Delete sentiment config
      operationId: deleteSentiment
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Sentiment deleted

  /dashboard/api/sentiment-stats:
    post:
      tags: [Sentiments]
      summary: Get sentiment distribution stats
      operationId: getSentimentStats
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryFilters'
      responses:
        "200":
          description: Sentiment distribution with counts per tag
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsTagStats'

  /dashboard/api/sentiment-messages:
    post:
      tags: [Sentiments]
      summary: Get messages matching an active or suggested intent tag
      description: >-
        Proactively suggested intents expose up to 20 stored evidence matches
        before approval. Active and suggested matches honor time_range and
        metadata filters. Each message includes the authoritative conversation
        user_id and user_email when available.
      operationId: getMessagesByTag
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [tag]
                  properties:
                    tag:
                      type: string
      responses:
        "200":
          description: Active intent matches or suggested-intent evidence preview

  /dashboard/api/sentiment-timeline:
    post:
      tags: [Sentiments]
      summary: Get timeline for a single sentiment tag
      operationId: getTagTimeline
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [tag]
                  properties:
                    tag:
                      type: string
      responses:
        "200":
          description: Daily event counts for the tag

  /dashboard/api/sentiment-timelines:
    post:
      tags: [Sentiments]
      summary: Get timelines for multiple tags (batch)
      operationId: getBatchTagTimelines
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [tags]
              properties:
                tags:
                  type: array
                  items:
                    type: string
                time_range:
                  $ref: '#/components/schemas/TimeRange'
      responses:
        "200":
          description: Timelines keyed by tag name
          content:
            application/json:
              schema:
                type: object
                properties:
                  timelines:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: object
                        properties:
                          timestamp:
                            type: string
                          value:
                            type: number

  /dashboard/api/summarize-intent-distribution:
    post:
      tags: [Sentiments]
      summary: LLM summary of an intent distribution
      description: |
        Pulls user inputs matching the given intent within the time range and
        produces a natural-language summary via the LLM.
      operationId: summarizeIntentDistribution
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                org_id:
                  type: string
                  format: uuid
                tag:
                  type: string
                  description: Intent title to summarize
                time_range:
                  $ref: '#/components/schemas/TimeRange'
      responses:
        "200":
          description: Natural-language summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: string

  /dashboard/api/run-intent-classification:
    post:
      tags: [Classification]
      summary: Run intent classification on conversations
      operationId: runIntentClassification
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClassificationRequest'
      responses:
        "200":
          description: Classification results

  /dashboard/api/run-sop-classification:
    post:
      tags: [Classification]
      summary: Run SOP classification on conversations
      operationId: runSOPClassification
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClassificationRequest'
      responses:
        "200":
          description: SOP classification results

  /dashboard/api/run-spotlight-query:
    post:
      tags: [Classification]
      summary: Run LLM-based spotlight query (SSE streaming)
      operationId: runSpotlightLLMQuery
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QueryFilters'
                - type: object
                  required: [query]
                  properties:
                    query:
                      type: string
      responses:
        "200":
          description: SSE stream of classification results
          content:
            text/event-stream:
              schema:
                type: string

  /dashboard/api/alerts:
    get:
      tags: [Alerts]
      summary: List alerts
      operationId: listAlerts
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: List of custom, daily summary, and proactive suggested-intent alert configurations.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AlertConfig'
    post:
      tags: [Alerts]
      summary: Create alert
      description: Custom alerts require a validated `context_sql_query` in addition to the scalar condition `sql_query`.
      operationId: createAlert
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AlertConfigInput'
      responses:
        "201":
          description: Alert created

  /dashboard/api/alerts/{id}:
    get:
      tags: [Alerts]
      summary: Get alert by ID
      operationId: getAlert
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Alert configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertConfig'
    put:
      tags: [Alerts]
      summary: Update alert
      operationId: updateAlert
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AlertConfigInput'
      responses:
        "200":
          description: Alert updated
    delete:
      tags: [Alerts]
      summary: Delete alert
      operationId: deleteAlert
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Alert deleted

  /dashboard/api/alerts/{id}/toggle:
    post:
      tags: [Alerts]
      summary: Toggle alert active status
      operationId: toggleAlert
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Alert toggled

  /dashboard/api/alerts/{id}/history:
    get:
      tags: [Alerts]
      summary: Get alert execution history
      operationId: getAlertHistory
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Last 50 alert executions

  /dashboard/api/alerts/generate-query:
    post:
      tags: [Alerts]
      summary: Generate alert SQL from natural language (LLM)
      operationId: generateAlertQuery
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [natural_language_query]
              properties:
                natural_language_query:
                  type: string
      responses:
        "200":
          description: Generated SQL query

  /dashboard/api/alerts/build-query:
    post:
      tags: [Alerts]
      summary: Build alert SQL from structured params (no LLM)
      operationId: buildAlertQuery
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [params]
              properties:
                params:
                  type: object
      responses:
        "200":
          description: Built SQL query

  /dashboard/api/alerts/test-query:
    post:
      tags: [Alerts]
      summary: Test a SQL query and return results
      operationId: testAlertQuery
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [sql_query]
              properties:
                sql_query:
                  type: string
                query_type:
                  type: string
                  enum: [condition, action]
                  default: condition
      responses:
        "200":
          description: Query results

  /dashboard/api/alerts/{id}/test:
    post:
      tags: [Alerts]
      summary: Send a test notification for an existing alert
      description: |
        Fires the alert's notification action immediately, with sample/test
        context, so the user can verify the wiring (Slack channel, email
        recipients, message format) without waiting for the next scheduled
        evaluation.
      operationId: sendTestNotification
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Test notification sent

  /dashboard/api/alerts/summarize-action:
    post:
      tags: [Alerts]
      summary: LLM summarize action query results
      operationId: summarizeAction
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [action_prompt, rows]
              properties:
                action_prompt:
                  type: string
                rows:
                  type: string
      responses:
        "200":
          description: Summary text

  /dashboard/api/report-config:
    get:
      tags: [Alerts]
      summary: Get report configuration
      operationId: getReportConfig
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: Report configuration
    put:
      tags: [Alerts]
      summary: Update report configuration
      operationId: saveReportConfig
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                delivery_time:
                  type: string
                  pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
                  description: UTC delivery time; the dashboard converts it to and from browser-local time.
      responses:
        "200":
          description: Report config saved

  /dashboard/api/dashboard/charts:
    get:
      tags: [Dashboard]
      summary: List saved dashboard charts
      operationId: listDashboardCharts
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: List of chart configurations
    post:
      tags: [Dashboard]
      summary: Create dashboard chart
      operationId: createDashboardChart
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        "201":
          description: Chart created

  /dashboard/api/dashboard/charts/{id}:
    delete:
      tags: [Dashboard]
      summary: Delete dashboard chart
      operationId: deleteDashboardChart
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Chart deleted

  /dashboard/api/dashboard/charts/reorder:
    put:
      tags: [Dashboard]
      summary: Reorder dashboard charts
      operationId: reorderDashboardCharts
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [chart_ids]
              properties:
                chart_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
      responses:
        "200":
          description: Charts reordered

  /dashboard/api/organization/event-keys:
    get:
      tags: [Organizations]
      summary: Get event metadata key configuration
      operationId: getEventKeys
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: Event key configuration
    put:
      tags: [Organizations]
      summary: Update event metadata keys
      operationId: updateEventKeys
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                keys:
                  type: array
                  items:
                    type: string
                include_keys:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Event keys updated

  /dashboard/api/organization/conversation-keys:
    get:
      tags: [Organizations]
      summary: Get conversation and user metadata keys
      operationId: getConversationKeys
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: Conversation key configuration
          content:
            application/json:
              schema:
                type: object
                required:
                  - conversation_keys
                  - conversation_metadata_keys
                  - user_metadata_keys
                properties:
                  conversation_keys:
                    type: array
                    description: Legacy combined registry of conversation and user metadata keys.
                    items:
                      type: string
                  conversation_metadata_keys:
                    type: array
                    description: Keys discovered from conversation metadata.
                    items:
                      type: string
                  user_metadata_keys:
                    type: array
                    description: Keys discovered from conversation user metadata.
                    items:
                      type: string
    put:
      tags: [Organizations]
      summary: Update conversation metadata keys
      operationId: updateConversationKeys
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                keys:
                  type: array
                  items:
                    type: string
                include_keys:
                  type: array
                  items:
                    type: string
      responses:
        "200":
          description: Conversation keys updated

  /dashboard/api/organization/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys
      operationId: listAPIKeys
      security:
        - BearerAuth: []
      responses:
        "200":
          description: List of API keys (hashes, not raw keys)
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_keys:
                    type: array
                    items:
                      $ref: '#/components/schemas/APIKeyRecord'
    post:
      tags: [API Keys]
      summary: Create API key
      operationId: createAPIKey
      security:
        - BearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "201":
          description: API key created (raw key returned ONCE)
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key:
                    type: string
                    description: "Raw API key. Store securely, shown only once"

  /dashboard/api/organization/api-keys/{id}:
    delete:
      tags: [API Keys]
      summary: Delete API key
      operationId: deleteAPIKey
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: API key deleted

  /dashboard/api/connections:
    get:
      tags: [Connections]
      summary: List connections
      operationId: listConnections
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      responses:
        "200":
          description: List of external integrations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Connection"

  /dashboard/api/connections/initiate-slack:
    post:
      tags: [Connections]
      summary: Initiate Slack OAuth flow
      operationId: initiateSlackOAuth
      security:
        - BearerAuth: []
          OrgScope: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                org_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Slack OAuth URL
          content:
            application/json:
              schema:
                type: object
                required: [oauth_url]
                properties:
                  oauth_url:
                    type: string
                    format: uri

  /dashboard/api/connections/{id}:
    delete:
      tags: [Connections]
      summary: Delete connection
      operationId: deleteConnection
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Connection disconnected and detached from alert/report configurations
          content:
            application/json:
              schema:
                type: object
                required: [success]
                properties:
                  success:
                    type: boolean

  /dashboard/api/connections/{id}/verify:
    post:
      tags: [Connections]
      summary: Verify Slack bot channel access
      operationId: verifySlackConnection
      description: Sends a visible test message as the bot and refreshes connection readiness. A 409 response means the bot still needs a channel invite or Slack rejected the post.
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Bot posting and mention follow-ups are ready
          content:
            application/json:
              schema:
                type: object
                required: [success, status]
                properties:
                  success:
                    type: boolean
                  status:
                    type: string
                    enum: [ready]
        "409":
          description: Bot cannot yet post in the selected channel
          content:
            application/json:
              schema:
                type: object
                required: [error, status]
                properties:
                  error:
                    type: string
                  status:
                    type: string
                    enum: [needs_invite, degraded]

  /dashboard/api/connections/{id}/send-message:
    post:
      tags: [Connections]
      summary: Send message via Slack connection
      operationId: sendSlackMessage
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                channel:
                  type: string
      responses:
        "200":
          description: Message sent

  /dashboard/api/billing/create-checkout:
    post:
      tags: [Billing]
      summary: Create Stripe checkout session
      operationId: createCheckoutSession
      security:
        - BearerAuth: []
          OrgScope: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [org_id]
              properties:
                org_id:
                  type: string
                  format: uuid
                tier:
                  type: string
                  enum: [starter, pro]
                  default: pro
      responses:
        "200":
          description: Stripe checkout URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        "400":
          description: Invalid or unconfigured checkout tier

  /dashboard/api/billing/create-portal:
    post:
      tags: [Billing]
      summary: Create Stripe billing portal session
      operationId: createPortalSession
      security:
        - BearerAuth: []
          OrgScope: []
      responses:
        "200":
          description: Stripe portal URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri

  /dashboard/api/billing/cancel:
    post:
      tags: [Billing]
      summary: Cancel subscription
      operationId: cancelSubscription
      security:
        - BearerAuth: []
          OrgScope: []
      responses:
        "200":
          description: Subscription cancelled

  /dashboard/api/onboarding/describe-product:
    post:
      tags: [Onboarding]
      summary: Stream an LLM-generated product description from a website
      description: |
        Fetches the user's website (either a URL passed directly or inferred
        from a corporate email domain) and streams a 2-3 sentence product
        description over Server-Sent Events. Used during organization setup
        so the user does not have to write the description by hand.

        Returns 422 if the email domain is a free email provider, the site
        cannot be reached, or extraction yielded too little content. The
        client should ask the user to describe the product manually in those
        cases.

        Response is `text/event-stream`. Each event is a JSON object:
        `{"text": "..."}` for incremental tokens, `{"error": "..."}` for
        terminal failure. The stream ends with a `[DONE]` sentinel.
      operationId: onboardingDescribeProduct
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  description: Website URL (preferred). Either url or email is required.
                email:
                  type: string
                  format: email
                  description: Falls back to the email's domain if url is not provided.
      responses:
        "200":
          description: Server-Sent Events stream of description tokens
          content:
            text/event-stream:
              schema:
                type: string
        "422":
          description: Personal email, unreachable site, or insufficient content

  /auth/callback:
    get:
      tags: [Auth]
      summary: Google OAuth callback (redirect)
      operationId: googleOAuthCallbackGet
      security: []
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirects to frontend with JWT token
    post:
      tags: [Auth]
      summary: Google OAuth callback (JSON response)
      operationId: googleOAuthCallbackPost
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
      responses:
        "200":
          description: JWT token
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string

  /auth/slack/callback:
    get:
      tags: [Auth]
      summary: Slack OAuth callback
      operationId: slackOAuthCallback
      security: []
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirects to frontend

  /webhooks/stripe:
    post:
      tags: [System]
      summary: Stripe webhook
      operationId: stripeWebhook
      security: []
      description: Verified via Stripe-Signature header
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Webhook processed
        "400":
          description: Invalid signature


  /dashboard/api/classify:
    post:
      tags: [Classification]
      summary: Unified classification endpoint
      description: |
        Dispatches to intent, SOP, or spotlight classification based on type field.
        Replaces separate run-intent-classification, run-sop-classification, and run-spotlight-query endpoints.
      operationId: runClassification
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/QueryFilters"
                - type: object
                  required: [type]
                  properties:
                    type:
                      type: string
                      enum: [intent, sop, spotlight]
                    query:
                      type: string
                      description: Required for spotlight type
      responses:
        "200":
          description: Classification results

  /dashboard/api/alerts/query:
    post:
      tags: [Alerts]
      summary: Unified alert query endpoint
      description: |
        Dispatches to generate, build, test, or summarize based on action field.
        Replaces separate generate-query, build-query, test-query, and summarize-action endpoints.
      operationId: alertQuery
      security:
        - BearerAuth: []
          OrgScope: []
        - ApiKey: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [generate, build, test, summarize]
                natural_language_query:
                  type: string
                  description: For generate action
                params:
                  type: object
                  description: For build action
                sql_query:
                  type: string
                  description: For test action
                query_type:
                  type: string
                  enum: [condition, action]
                  description: For test action
                action_prompt:
                  type: string
                  description: For summarize action
                rows:
                  type: string
                  description: For summarize action
      responses:
        "200":
          description: Query operation results
