openapi: 3.0.3
info:
  title: Qwairy API
  description: |
    Track your brand's visibility across AI platforms like ChatGPT, Claude, Perplexity, and Gemini.

    ## Authentication
    All endpoints require a Bearer token in the Authorization header:
    ```
    Authorization: Bearer qw-api-xxxxxxxxxxxxx
    ```

    Generate API tokens from Team Settings > Integrations > API Access.
    Tokens require an active team and an eligible active subscription (Growth, Business,
    Enterprise, Agency Growth, or Agency Business), including a parent team's subscription.
    Missing or revoked tokens and ineligible subscriptions return HTTP 401.

    ## Rate Limiting
    - Default: 1,000 requests per minute per team
    - Default: 20,000 requests per day per team
    - Team-specific limits may override these defaults
    - Quota headers are returned only after authentication, paid-surface admission, and a completed rate-limit check
    - Both successful responses and errors after that check include the checked burst and daily windows
    - Authentication refusals and paid-surface refusals before the check have no quota headers
    - `RateLimit-Policy` and `RateLimit` are omitted if either window check times out or fails
    - Legacy `X-RateLimit-*` fields can contain zero values on a resolved limiter timeout; use the standard fields for verified quotas
    - `RateLimit-Policy` and `RateLimit` follow sections 3 and 4 of
      [draft-ietf-httpapi-ratelimit-headers-11](https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-11.html),
      an IETF Internet-Draft, not a final RFC
    - Each field is a Structured Fields list of strings with parameters, for example:
      `RateLimit-Policy: "burst";q=1000;w=60, "daily";q=20000;w=86400` and
      `RateLimit: "burst";r=999;t=60, "daily";r=19999;t=86400`
    - `q` is the allocated request quota, `w` is the window in seconds, `r` is the remaining quota,
      and `t` is a non-negative integer delta in seconds until the end of the current effective window, not an absolute timestamp
    - `t` is a limiter window hint and does not guarantee complete quota renewal when that window ends
    - Legacy `X-RateLimit-*` burst and daily fields remain available; their reset fields are ISO 8601 timestamps
    - On HTTP 429, honor `Retry-After` for the blocking window (burst or daily); a positive remaining quota is not a guarantee of admission

    ## Base URL
    ```
    https://www.qwairy.co/api/v1
    ```
  version: 1.0.0
  contact:
    email: team@qwairy.co
    url: https://qwairy.co
  license:
    name: Proprietary
    url: https://qwairy.co/legal/terms

servers:
  - url: https://www.qwairy.co/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Brands
    description: Manage and list brands in your account
  - name: Performance
    description: Brand performance metrics and GEO scores
  - name: Crawler Analytics
    description: Aggregated AI crawler activity observed in server logs
  - name: Competitors
    description: Track competitor mentions and share of voice
  - name: Sources
    description: Monitor content sources cited by AI platforms
  - name: Prompts
    description: Access monitored prompts and queries
  - name: Answers
    description: Retrieve AI-generated responses and analysis
  - name: Follow-Ups
    description: Retrieve next questions suggested by AI providers
  - name: Search
    description: Query fan-out and AI search insights
  - name: Keywords
    description: Track keyword/topic performance metrics
  - name: Tags
    description: Track tag performance metrics

paths:
  /brands:
    get:
      tags: [Brands]
      summary: List active brands
      description: Returns your team's active brands that are not being transferred
      operationId: listBrands
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  total:
                    type: integer
                    example: 2
                  brands:
                    type: array
                    items:
                      $ref: '#/components/schemas/BrandSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}:
    get:
      tags: [Brands]
      summary: Get brand details
      description: Returns detailed information about an active brand that is not being transferred
      operationId: getBrand
      parameters:
        - $ref: '#/components/parameters/brandId'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  brand:
                    $ref: '#/components/schemas/BrandDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/performance:
    get:
      tags: [Performance]
      summary: Get performance snapshot
      description: Returns aggregated performance metrics for a brand
      operationId: getPerformance
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - name: startDate
          in: query
          description: ISO 8601 date or timestamp, provided together with endDate. Local timestamps mean UTC. Invalid, empty, incomplete or inverted ranges return 400. An explicit range takes precedence over period.
          schema:
            type: string
        - name: endDate
          in: query
          description: ISO 8601 date or timestamp, provided together with startDate and not earlier than it. Includes the rest of the end date's UTC day. Local timestamps mean UTC. Invalid, empty, incomplete or inverted ranges return 400.
          schema:
            type: string
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerformanceSnapshot'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/crawler-analytics:
    get:
      tags: [Crawler Analytics]
      summary: Get AI crawler analytics
      description: |
        Returns retained daily AI crawler aggregates for a brand, including activity over
        time, crawler distribution, page states, HTTP status groups, coverage, and pagination.
        Page detail is available for 30 days; first seen, last crawled, and lifetime volume
        remain available from the durable page inventory. Raw server logs and human traffic
        are neither returned nor retained.
        By default the pages array lists only pages with at least one occurrence in the
        range, and pagination totals describe that list. Set activeOnly=false to list the
        durable page inventory instead, including pages without occurrences in the range.
      operationId: getCrawlerAnalytics
      parameters:
        - $ref: '#/components/parameters/brandId'
        - name: range
          in: query
          description: Retained range. Defaults to 30d.
          schema:
            type: string
            enum: [24h, 7d, 30d]
            default: 30d
        - name: state
          in: query
          description: Filter the listed pages by computed state. Applies to active pages by default, and to the durable inventory with activeOnly=false.
          schema:
            type: string
            enum: [all, new, hot]
            default: all
        - name: activeOnly
          in: query
          description: |
            Defaults to true, also when omitted: only pages with at least one occurrence in
            the range are listed. Set to false to list the durable page inventory, including
            pages without occurrences in the range. That listing ranks and counts every known
            page and is markedly slower on large inventories.
          schema:
            type: boolean
            default: true
        - name: page
          in: query
          description: One-based page number for the pages array.
          schema:
            type: integer
            minimum: 1
            maximum: 10000
            default: 1
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Aggregated crawler analytics with explicit coverage semantics
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrawlerAnalyticsResponse'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientPlan'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          description: An unexpected server error occurred
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/InternalServerError'
                  - $ref: '#/components/schemas/Error'

  /brands/{brandId}/competitors:
    get:
      tags: [Competitors]
      summary: List competitors
      description: Returns competitors with metrics, excluding ignored competitors. Defaults to SELF and DIRECT relationships.
      operationId: listCompetitors
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
        - name: relationship
          in: query
          description: Filter by comma-separated SELF, DIRECT, or INDIRECT values. Defaults to SELF and DIRECT.
          schema:
            type: string
            default: SELF,DIRECT
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [mentions, position, sentiment, shareOfVoice, name]
            default: mentions
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  competitors:
                    type: array
                    items:
                      $ref: '#/components/schemas/Competitor'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/competitors/{competitorId}:
    get:
      tags: [Competitors]
      summary: Get competitor details
      description: Returns detailed metrics for a specific competitor
      operationId: getCompetitor
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/competitorId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  competitor:
                    $ref: '#/components/schemas/CompetitorDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/CompetitorNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/competitors/{competitorId}/evolution:
    get:
      tags: [Competitors]
      summary: Get competitor evolution
      description: Returns daily evolution data for a competitor
      operationId: getCompetitorEvolution
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/competitorId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompetitorEvolution'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/CompetitorNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/source-domains:
    get:
      tags: [Sources]
      summary: List sources
      description: Returns cited source domains with their performance metrics
      operationId: listSources
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - name: type
          in: query
          description: |
            Filter by source type. Recognized values are INSTITUTIONAL, COMMERCIAL,
            MEDIA, BLOG, FORUM, SOCIAL, EDUCATIONAL, REFERENCE, and OTHER.
            Unrecognized values return no matching sources.
          schema:
            type: string
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [mentions, position, rate, domain]
            default: mentions
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  sources:
                    type: array
                    items:
                      $ref: '#/components/schemas/Source'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/source-domains/{sourceId}:
    get:
      tags: [Sources]
      summary: Get source details
      description: Returns detailed metrics for a specific cited source domain
      operationId: getSource
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/sourceId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  source:
                    $ref: '#/components/schemas/SourceDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SourceNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/source-domains/{sourceId}/evolution:
    get:
      tags: [Sources]
      summary: Get source evolution
      description: Returns daily evolution data for a cited source domain
      operationId: getSourceEvolution
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/sourceId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourceEvolution'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SourceNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/prompts:
    get:
      tags: [Prompts]
      summary: List prompts
      description: Returns prompts (questions) analyzed for this brand. Countries and personas filter the effective current prompt configuration, not historical answer metrics.
      operationId: listPrompts
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
        - $ref: '#/components/parameters/personaIds'
        - $ref: '#/components/parameters/configuredCountries'
        - name: include
          in: query
          description: Set to details to include aggregated competitor and source statistics.
          schema:
            type: string
            enum: [details]
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [lastGeneratedAt, answersCount, text, createdAt]
            default: lastGeneratedAt
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  prompts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Prompt'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/prompts/{promptId}:
    get:
      tags: [Prompts]
      summary: Get prompt details
      description: Returns detailed information about a specific prompt
      operationId: getPrompt
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/promptId'
        - name: include
          in: query
          description: Set to details to include aggregated competitor and source statistics.
          schema:
            type: string
            enum: [details]
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  prompt:
                    $ref: '#/components/schemas/PromptDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/PromptNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/prompts/{promptId}/answers:
    get:
      tags: [Prompts]
      summary: Get prompt answers
      description: Returns all AI-generated responses for a specific prompt
      operationId: getPromptAnswers
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/promptId'
        - $ref: '#/components/parameters/provider'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  prompt:
                    type: object
                    properties:
                      id:
                        type: string
                      text:
                        type: string
                  total:
                    type: integer
                  answers:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnswerFull'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/PromptNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/search:
    get:
      tags: [Search]
      summary: List query fan-outs
      description: Returns aggregated web search queries generated by AI providers, grouped by query text.
      operationId: listSearchInsights
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [occurrences, priority, brandPresence, query, createdAt]
            default: occurrences
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  searches:
                    type: array
                    items:
                      $ref: '#/components/schemas/SearchInsight'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/follow-ups:
    get:
      tags: [Follow-Ups]
      summary: List Follow-Ups
      description: Returns the next questions suggested after monitored AI answers, with their source prompt and provider provenance. Countries and personas filter the immutable historical source answer.
      operationId: listFollowUps
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
        - $ref: '#/components/parameters/personaIds'
        - $ref: '#/components/parameters/countries'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  followUps:
                    type: array
                    items:
                      type: object
                      required:
                        [
                          id,
                          text,
                          types,
                          position,
                          question,
                          answer,
                          isMonitored,
                        ]
                      properties:
                        id: { type: string, format: uuid }
                        text: { type: string }
                        kind:
                          type: string
                          enum: [SUGGESTION, GUIDED_CONTINUATION]
                          description: SUGGESTION is a prompt the engine offers as is. GUIDED_CONTINUATION is the message ChatGPT sends once the user fills in the form it displays; its text names each form field as a {placeholder}.
                        types:
                          type: array
                          items:
                            $ref: '#/components/schemas/SignalType'
                        position: { type: integer }
                        isMonitored: { type: boolean }
                        question:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            text: { type: string }
                        answer:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            providerId: { type: string, nullable: true }
                            modelId: { type: string }
                            createdAt: { type: string, format: date-time }
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/answers:
    get:
      tags: [Answers]
      summary: List answers
      description: Returns generated answers with filters. Countries and personas filter the immutable historical answer, independently of current prompt configuration.
      operationId: listAnswers
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
        - $ref: '#/components/parameters/personaIds'
        - $ref: '#/components/parameters/countries'
        - name: hasSelfMention
          in: query
          description: Filter by presence of brand mention
          schema:
            type: boolean
        - name: hasSelfSource
          in: query
          description: Filter by presence of brand source citation
          schema:
            type: boolean
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [createdAt]
            default: createdAt
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  answers:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnswerSummary'
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/answers/{answerId}:
    get:
      tags: [Answers]
      summary: Get answer details
      description: Returns the complete response with full text and detailed analysis
      operationId: getAnswer
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/answerId'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  answer:
                    $ref: '#/components/schemas/AnswerDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AnswerNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/keywords:
    get:
      tags: [Keywords]
      summary: List keywords
      description: Returns keyword/topic performance metrics with share of voice, mention rate, and sentiment.
      operationId: listKeywords
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum:
              [prompts, answers, mentionRate, shareOfVoice, sentiment, keyword]
            default: prompts
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  keywords:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        keyword:
                          type: string
                        totalPrompts:
                          type: integer
                        totalAnswers:
                          type: integer
                        brandMentionRate:
                          type: number
                          description: Percentage of answers mentioning SELF among answers mentioning SELF or DIRECT competitors.
                        shareOfVoice:
                          type: number
                          description: Share of voice percentage (SELF / (SELF + DIRECT))
                        avgSentiment:
                          type: number
                          description: Average sentiment score (-100 to +100)
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/keywords/{keywordId}/evolution:
    get:
      tags: [Keywords]
      summary: Get keyword evolution
      description: Returns daily time-series metrics for a specific keyword/topic.
      operationId: getKeywordEvolution
      parameters:
        - $ref: '#/components/parameters/brandId'
        - name: keywordId
          in: path
          required: true
          description: The keyword ID
          schema:
            type: string
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/tag'
        - $ref: '#/components/parameters/promptType'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  keyword:
                    type: object
                    properties:
                      id:
                        type: string
                      text:
                        type: string
                  evolution:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        answers:
                          type: integer
                        mentionRate:
                          type: number
                        shareOfVoice:
                          type: number
                        avgSentiment:
                          type: number
                          nullable: true
                        sourceRate:
                          type: number
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Brand or keyword not found (BRAND_NOT_FOUND or KEYWORD_NOT_FOUND).
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/tags:
    get:
      tags: [Tags]
      summary: List tags
      description: Returns tag performance metrics with share of voice, mention rate, and sentiment.
      operationId: listTags
      parameters:
        - $ref: '#/components/parameters/brandId'
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - name: sort
          in: query
          schema:
            type: string
            enum: [prompts, answers, mentionRate, shareOfVoice, sentiment, name]
            default: prompts
        - $ref: '#/components/parameters/order'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  tags:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        totalPrompts:
                          type: integer
                        totalAnswers:
                          type: integer
                        brandMentionRate:
                          type: number
                        shareOfVoice:
                          type: number
                        avgSentiment:
                          type: number
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/BrandNotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /brands/{brandId}/tags/{tagId}/evolution:
    get:
      tags: [Tags]
      summary: Get tag evolution
      description: Returns daily time-series metrics for a specific tag.
      operationId: getTagEvolution
      parameters:
        - $ref: '#/components/parameters/brandId'
        - name: tagId
          in: path
          required: true
          description: The tag ID
          schema:
            type: string
        - $ref: '#/components/parameters/period'
        - $ref: '#/components/parameters/startDate'
        - $ref: '#/components/parameters/endDate'
        - $ref: '#/components/parameters/provider'
        - $ref: '#/components/parameters/topic'
        - $ref: '#/components/parameters/promptType'
      responses:
        '200':
          description: Successful response
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  tag:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                  evolution:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        answers:
                          type: integer
                        mentionRate:
                          type: number
                        shareOfVoice:
                          type: number
                        avgSentiment:
                          type: number
                          nullable: true
                        sourceRate:
                          type: number
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Brand or tag not found (BRAND_NOT_FOUND or TAG_NOT_FOUND).
          headers:
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitBurstLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitBurstRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitBurstReset'
            X-RateLimit-Daily-Limit:
              $ref: '#/components/headers/RateLimitDailyLimit'
            X-RateLimit-Daily-Remaining:
              $ref: '#/components/headers/RateLimitDailyRemaining'
            X-RateLimit-Daily-Reset:
              $ref: '#/components/headers/RateLimitDailyReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  headers:
    RateLimitPolicy:
      description: >-
        Checked team quotas as a Structured Fields list of strings with q (requests) and w (seconds).
        Follows draft-ietf-httpapi-ratelimit-headers-11 section 3, not a final RFC.
        Present only after a completed authenticated rate-limit check; team overrides apply.
      schema:
        type: string
        example: '"burst";q=1000;w=60, "daily";q=20000;w=86400'
    RateLimit:
      description: >-
        Checked remaining quotas as a Structured Fields list of strings with r (requests) and t
        (non-negative integer delta-seconds until the end of the current effective window, never a date).
        This limiter window hint does not guarantee complete quota renewal at its end.
        Follows draft-ietf-httpapi-ratelimit-headers-11 section 4, not a final RFC.
        Present only after a completed authenticated rate-limit check, including errors after consumption.
      schema:
        type: string
        example: '"burst";r=999;t=60, "daily";r=19999;t=86400'
    RateLimitBurstLimit:
      description: Legacy checked per-minute request quota. May contain zero values after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: integer
        minimum: 0
        example: 1000
    RateLimitBurstRemaining:
      description: Legacy remaining per-minute requests. May contain zero values after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: integer
        minimum: 0
        example: 999
    RateLimitBurstReset:
      description: Legacy per-minute reset as an absolute ISO 8601 timestamp. May contain the Unix epoch after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: string
        format: date-time
        example: '2026-10-24T12:01:00.000Z'
    RateLimitDailyLimit:
      description: Legacy checked daily request quota. May contain zero values after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: integer
        minimum: 0
        example: 20000
    RateLimitDailyRemaining:
      description: Legacy remaining daily requests. May contain zero values after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: integer
        minimum: 0
        example: 19999
    RateLimitDailyReset:
      description: Legacy daily reset as an absolute ISO 8601 timestamp. May contain the Unix epoch after a limiter timeout; use the standard fields for verified quotas.
      schema:
        type: string
        format: date-time
        example: '2026-10-25T12:00:00.000Z'

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Token
      description: 'API token with qw-api- prefix'

  parameters:
    brandId:
      name: brandId
      in: path
      required: true
      description: The unique identifier of the brand
      schema:
        type: string
    competitorId:
      name: competitorId
      in: path
      required: true
      description: The unique identifier of the competitor
      schema:
        type: string
    sourceId:
      name: sourceId
      in: path
      required: true
      description: The unique identifier of the source
      schema:
        type: string
    promptId:
      name: promptId
      in: path
      required: true
      description: The unique identifier of the prompt
      schema:
        type: string
    answerId:
      name: answerId
      in: path
      required: true
      description: The unique identifier of the answer
      schema:
        type: string
    period:
      name: period
      in: query
      description: Number of days to include. No default; returns all data if neither period nor a complete date range is specified. SQL-based date filters cap this at 3650 days. Avoid combining period with a date range; precedence varies by operation.
      schema:
        type: integer
        minimum: 1
    startDate:
      name: startDate
      in: query
      description: Start date or datetime (ISO 8601). Provide together with endDate; an isolated date is ignored.
      schema:
        type: string
    endDate:
      name: endDate
      in: query
      description: End date or datetime (ISO 8601). Provide together with startDate; an isolated date is ignored.
      schema:
        type: string
    provider:
      name: provider
      in: query
      description: Filter by AI provider. Supports comma-separated values for multi-select (e.g., "chatgpt,claude").
      schema:
        type: string
        example: chatgpt,claude
    topic:
      name: topic
      in: query
      description: Filter by topic (keyword) ID. Supports comma-separated values for multi-select (e.g., "id1,id2").
      schema:
        type: string
    tag:
      name: tag
      in: query
      description: Filter by tag ID. Supports comma-separated values for multi-select (e.g., "id1,id2").
      schema:
        type: string
    promptType:
      name: type
      in: query
      description: Filter by prompt type (funnel stage)
      schema:
        type: string
        enum: [TOFU, MOFU, BOFU, POST_PURCHASE]
    personaIds:
      name: personaIds
      in: query
      description: Filter by comma-separated persona UUIDs. On /prompts this matches current configured personas; `default` means no QuestionPersona association. On /answers and /follow-ups this matches the historical answer persona; `default` means an answer generated without a persona.
      schema:
        type: string
        example: 11111111-1111-4111-8111-111111111111,default
    countries:
      name: countries
      in: query
      description: Filter /answers or /follow-ups by the immutable historical Answer.countryCode snapshot. Accepts up to 50 comma-separated ISO 3166-1 alpha-2 entries before deduplication. Entries are trimmed, uppercased and deduplicated; any invalid entry returns HTTP 400. An absent or empty value applies no filter. A valid country without data returns no results.
      schema:
        type: string
        example: FR,BE
    configuredCountries:
      name: countries
      in: query
      description: Filter /prompts by effective currently configured countries, not historical answers. Prompt countries are intersected with the current workspace country catalog; an empty or entirely obsolete configuration falls back to the workspace default country. Answer metrics are not scoped by country. Accepts up to 50 comma-separated ISO 3166-1 alpha-2 entries before deduplication. Entries are trimmed, uppercased and deduplicated; any invalid entry returns HTTP 400. An absent or empty value applies no filter. A valid country without matching configuration returns no results.
      schema:
        type: string
        example: FR,BE
    limit:
      name: limit
      in: query
      description: Maximum number of results (default 50, max 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    offset:
      name: offset
      in: query
      description: Number of results to skip for pagination
      schema:
        type: integer
        minimum: 0
        default: 0
    order:
      name: order
      in: query
      description: Sort order
      schema:
        type: string
        enum: [asc, desc]
        default: desc

  schemas:
    SignalType:
      type: string
      enum:
        - INFORMATIONAL
        - RECOMMENDATION
        - COMPARISON
        - HOW_TO
        - PROBLEM_SOLVING
        - TRANSACTIONAL
        - NAVIGATIONAL
        - LOCAL
        - COMMERCE
        - OTHER

    Persona:
      type: object
      required: [id, name, description]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true

    BrandSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type: string
          nullable: true

    BrandDetail:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        stats:
          type: object
          properties:
            promptsCount:
              type: integer
            answersCount:
              type: integer
            competitorsCount:
              type: integer

    Pagination:
      type: object
      properties:
        total:
          type: integer
          description: Total number of items matching filters
        count:
          type: integer
          description: Number of items in this response
        limit:
          type: integer
          description: Maximum items per page
        offset:
          type: integer
          description: Number of items skipped

    QueryFanOutPrompt:
      type: object
      properties:
        id:
          type: string
          description: Prompt ID
        text:
          type: string
          description: Prompt text

    SearchInsight:
      type: object
      properties:
        query:
          type: string
        occurrences:
          type: integer
        priority:
          type: string
          enum: [very-high, high, medium, low]
        brandPresence:
          type: integer
          description: Number of responses where the brand is present
        competitorPresence:
          type: integer
          description: Number of responses where direct competitors are present
        competitors:
          type: array
          items:
            type: string
        uniquePrompts:
          type: integer
        prompts:
          type: array
          items:
            $ref: '#/components/schemas/QueryFanOutPrompt'
        uniqueAnswers:
          type: integer
        uniqueModels:
          type: integer
        models:
          type: array
          items:
            type: string
        topics:
          type: array
          items:
            type: string
        tags:
          type: array
          items:
            type: string
        firstSeen:
          type: string
          format: date-time
          nullable: true
        lastSeen:
          type: string
          format: date-time
          nullable: true

    AnswerQueryFanOut:
      type: object
      properties:
        id:
          type: string
        query:
          type: string
        model:
          type: string
        createdAt:
          type: string
          format: date-time

    PromptQueryFanOut:
      type: object
      properties:
        query:
          type: string
        occurrences:
          type: integer
        uniqueAnswers:
          type: integer
        models:
          type: array
          items:
            type: string
        firstSeen:
          type: string
          format: date-time
          nullable: true
        lastSeen:
          type: string
          format: date-time
          nullable: true

    Competitor:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        domain:
          type: string
          nullable: true
        relationship:
          type: string
          enum: [SELF, DIRECT, INDIRECT, ECOSYSTEM, COMPLEMENTARY]
        totalMentions:
          type: integer
        shareOfVoice:
          type: number
          format: float
        avgPosition:
          type: number
          format: float
          nullable: true
        avgSentiment:
          type: number
          format: float
          nullable: true

    CompetitorDetail:
      allOf:
        - $ref: '#/components/schemas/Competitor'
        - type: object
          properties:
            byProvider:
              type: array
              items:
                type: object
                properties:
                  provider:
                    type: string
                  mentions:
                    type: integer
                  avgPosition:
                    type: number
                    format: float
                    nullable: true
            byTopic:
              type: array
              items:
                type: object
                properties:
                  topic:
                    type: string
                  mentions:
                    type: integer

    CompetitorEvolution:
      type: object
      properties:
        success:
          type: boolean
        competitor:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            relationship:
              type: string
        evolution:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              mentions:
                type: integer
              shareOfVoice:
                type: number
                format: float
              avgPosition:
                type: number
                format: float
                nullable: true
              avgSentiment:
                type: number
                format: float
                nullable: true

    Source:
      type: object
      properties:
        id:
          type: string
        domain:
          type: string
        type:
          type: string
          enum:
            [
              INSTITUTIONAL,
              COMMERCIAL,
              MEDIA,
              BLOG,
              FORUM,
              SOCIAL,
              EDUCATIONAL,
              REFERENCE,
              OTHER,
            ]
        isSelf:
          type: boolean
        totalMentions:
          type: integer
        rate:
          type: number
          format: float
        avgPosition:
          type: number
          format: float
          nullable: true
        aiSourceAuthorityScore:
          type: number
          nullable: true

    SourceDetail:
      allOf:
        - $ref: '#/components/schemas/Source'
        - type: object
          properties:
            status:
              type: string
            byProvider:
              type: array
              items:
                type: object
                properties:
                  provider:
                    type: string
                  mentions:
                    type: integer
                  avgPosition:
                    type: number
                    format: float
                    nullable: true
            byTopic:
              type: array
              items:
                type: object
                properties:
                  topic:
                    type: string
                  mentions:
                    type: integer
                  avgPosition:
                    type: number
                    format: float
                    nullable: true

    SourceEvolution:
      type: object
      properties:
        success:
          type: boolean
        source:
          type: object
          properties:
            id:
              type: string
            domain:
              type: string
            isSelf:
              type: boolean
        evolution:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              mentions:
                type: integer
              avgPosition:
                type: number
                format: float
                nullable: true

    Prompt:
      type: object
      allOf:
        - $ref: '#/components/schemas/PromptEnrichment'
      properties:
        id:
          type: string
        text:
          type: string
        topic:
          type: string
          nullable: true
        type:
          type: string
          enum: [TOFU, MOFU, BOFU, POST_PURCHASE]
          nullable: true
        tags:
          type: array
          items:
            type: string
        personas:
          type: array
          items:
            $ref: '#/components/schemas/Persona'
        modelIds:
          type: array
          description: Model IDs configured specifically for this prompt. An empty array means the workspace defaults apply.
          items:
            type: string
        monitoringEnabled:
          type: boolean
        frequency:
          type: string
          enum: [NONE, DAILY, WEEKLY, MONTHLY]
          nullable: true
          description: Effective monitoring frequency.
        frequencyInherited:
          type: boolean
        boostCount:
          type: integer
          minimum: 1
        answersCount:
          type: integer
        mentionRate:
          type: number
          format: float
        sourceRate:
          type: number
          format: float
        lastGeneratedAt:
          type: string
          format: date-time
          nullable: true
        competitors:
          type: array
          description: Returned only when include=details.
          items:
            allOf:
              - $ref: '#/components/schemas/PromptCompetitor'
              - type: object
                properties:
                  domain:
                    type: string
                    nullable: true
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: Returned only when include=details.

    PromptDetail:
      type: object
      allOf:
        - $ref: '#/components/schemas/PromptEnrichment'
      properties:
        id:
          type: string
        text:
          type: string
        topic:
          type: object
          nullable: true
          properties:
            id:
              type: string
            name:
              type: string
        type:
          type: string
          enum: [TOFU, MOFU, BOFU, POST_PURCHASE]
          nullable: true
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
        personas:
          type: array
          items:
            $ref: '#/components/schemas/Persona'
        modelIds:
          type: array
          description: Model IDs configured specifically for this prompt. An empty array means the workspace defaults apply.
          items:
            type: string
        monitoringEnabled:
          type: boolean
        frequency:
          type: string
          enum: [NONE, DAILY, WEEKLY, MONTHLY]
          nullable: true
          description: Effective monitoring frequency.
        frequencyInherited:
          type: boolean
        boostCount:
          type: integer
          minimum: 1
        answersCount:
          type: integer
        mentionRate:
          type: number
          format: float
        sourceRate:
          type: number
          format: float
        avgSentiment:
          type: number
          format: float
          nullable: true
        queryFanOutsTotal:
          type: integer
          description: Total number of query fan-outs for this prompt
        queryFanOutsLimit:
          type: integer
          description: Maximum number of query fan-outs returned
        queryFanOuts:
          type: array
          description: Top query fan-outs for this prompt, ordered by occurrences
          items:
            $ref: '#/components/schemas/PromptQueryFanOut'
        createdAt:
          type: string
          format: date-time
        lastGeneratedAt:
          type: string
          format: date-time
          nullable: true
        competitors:
          type: array
          description: Returned only when include=details.
          items:
            $ref: '#/components/schemas/PromptCompetitor'

    PromptEnrichment:
      type: object
      description: Additional optional properties returned only when include=details.
      properties:
        competitorsCount:
          type: integer
        competitorMentionsTotal:
          type: integer
        brandMentioned:
          type: boolean
        brandMentionCount:
          type: integer
        sources:
          type: array
          items:
            type: object
            properties:
              domain:
                type: string
              citations:
                type: integer
              isSelf:
                type: boolean
        sourcesCount:
          type: integer
        sourceCitationsTotal:
          type: integer
        sourceStability:
          type: number

    PromptCompetitor:
      type: object
      properties:
        name:
          type: string
        mentions:
          type: integer
        relationship:
          type: string
          enum: [SELF, DIRECT, INDIRECT, null]
          nullable: true

    AnswerStatus:
      type: string
      enum: [PROCESSING, COMPLETED, PARTIAL_ERROR]
      description: >-
        Analysis state of the answer. PROCESSING means the text is available but
        mentions, sources and sentiment are still being computed and may change.
        COMPLETED means every analysis finished. PARTIAL_ERROR means the text is
        available but at least one analysis failed, so these fields may be incomplete.

    AnswerSummary:
      type: object
      properties:
        id:
          type: string
        status:
          $ref: '#/components/schemas/AnswerStatus'
        promptId:
          type: string
        promptText:
          type: string
          nullable: true
        provider:
          type: string
        model:
          type: string
        persona:
          allOf:
            - $ref: '#/components/schemas/Persona'
          nullable: true
          description: Immutable persona snapshot used for generation, or null for the default persona.
        textPreview:
          type: string
          description: First 200 characters of the response, followed by ... when truncated (up to 203 characters).
        hasSelfMention:
          type: boolean
        selfMentionPosition:
          type: integer
          nullable: true
        hasSelfSource:
          type: boolean
        competitorsCount:
          type: integer
        sourcesCount:
          type: integer
        sentiment:
          type: number
          format: float
          nullable: true
        createdAt:
          type: string
          format: date-time

    AnswerFull:
      type: object
      properties:
        id:
          type: string
        status:
          $ref: '#/components/schemas/AnswerStatus'
        provider:
          type: string
        model:
          type: string
        persona:
          allOf:
            - $ref: '#/components/schemas/Persona'
          nullable: true
          description: Immutable persona snapshot used for generation, or null for the default persona.
        text:
          type: string
          nullable: true
        hasSelfMention:
          type: boolean
        selfMentionPosition:
          type: integer
          nullable: true
        hasSelfSource:
          type: boolean
        competitorsMentioned:
          type: array
          items:
            type: string
        sourcesCited:
          type: array
          items:
            type: string
        sentiment:
          type: number
          format: float
          nullable: true
        createdAt:
          type: string
          format: date-time

    AnswerDetail:
      type: object
      properties:
        id:
          type: string
        status:
          $ref: '#/components/schemas/AnswerStatus'
        prompt:
          type: object
          properties:
            id:
              type: string
              nullable: true
            text:
              type: string
              nullable: true
            topic:
              type: string
              nullable: true
            type:
              type: string
              nullable: true
        provider:
          type: string
        model:
          type: string
        persona:
          allOf:
            - $ref: '#/components/schemas/Persona'
          nullable: true
          description: Immutable persona snapshot used for generation, or null for the default persona.
        text:
          type: string
          nullable: true
        competitors:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                nullable: true
              position:
                type: integer
                nullable: true
              relationship:
                type: string
                enum: [SELF, DIRECT, INDIRECT, ECOSYSTEM, COMPLEMENTARY, null]
                nullable: true
              sentiment:
                type: number
                format: float
                nullable: true
        sources:
          type: array
          items:
            type: object
            properties:
              url:
                type: string
                nullable: true
              domain:
                type: string
                nullable: true
              position:
                type: integer
              isSelf:
                type: boolean
        queryFanOuts:
          type: array
          items:
            $ref: '#/components/schemas/AnswerQueryFanOut'
        sentiment:
          type: number
          format: float
          nullable: true
        createdAt:
          type: string
          format: date-time

    PerformanceSnapshot:
      type: object
      properties:
        success:
          type: boolean
        brand:
          $ref: '#/components/schemas/BrandSummary'
        period:
          type: object
          properties:
            start:
              type: string
              format: date
              nullable: true
            end:
              type: string
              format: date
              nullable: true
        methodology:
          type: object
          properties:
            promptsCount:
              type: integer
            providersCount:
              type: integer
            providers:
              type: array
              items:
                type: string
            responsesTotal:
              type: integer
            responsesWithMentions:
              type: integer
            responsesWithSources:
              type: integer
        scores:
          type: object
          properties:
            mentionRate:
              type: number
              format: float
            mentionCount:
              type: integer
            mentionTotal:
              type: integer
            coverage:
              type: number
              format: float
            sourceRate:
              type: number
              format: float
            sourceCount:
              type: integer
            sourceTotal:
              type: integer
            sourcePages:
              type: integer
            sentiment:
              type: number
              format: float
            shareOfVoice:
              type: number
              format: float
        topCompetitors:
          type: array
          maxItems: 5
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
                nullable: true
              relationship:
                type: string
                enum: [SELF, DIRECT]
              mentions:
                type: integer
              avgPosition:
                type: number
                format: float
                nullable: true
              avgSentiment:
                type: number
                format: float
                nullable: true
        topSources:
          type: array
          maxItems: 5
          items:
            type: object
            properties:
              id:
                type: string
              domain:
                type: string
              isSelf:
                type: boolean
              mentions:
                type: integer
              avgPosition:
                type: number
                format: float
                nullable: true
        byTopic:
          type: array
          description: >-
            Top topics of the scoped answers. Scores are the GEO Matrix Overall
            values for each topic over the same period and filters.
          items:
            type: object
            properties:
              id:
                type: string
              topic:
                type: string
              score:
                type: number
                format: float
                description: GEO Matrix Overall score (0-100).
              mentionRate:
                type: number
                format: float
              sourceRate:
                type: number
                format: float
                description: Brand citation rate used by the score.
              shareOfVoice:
                type: number
                format: float
              avgSentiment:
                type: number
                format: float
                nullable: true
                description: Average brand sentiment used by the score.
              promptsCount:
                type: integer
        byTag:
          type: array
          description: >-
            Tags carried by the scoped prompts. Scores are the GEO Matrix Overall
            values for each tag over the same period and filters.
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              score:
                type: number
                format: float
                description: GEO Matrix Overall score (0-100).
              mentionRate:
                type: number
                format: float
              sourceRate:
                type: number
                format: float
                description: Brand citation rate used by the score.
              shareOfVoice:
                type: number
                format: float
              avgSentiment:
                type: number
                format: float
                nullable: true
                description: Average brand sentiment used by the score.
              promptsCount:
                type: integer

    CrawlerAnalyticsResponse:
      type: object
      required:
        - success
        - rawEventsAvailable
        - humanTrafficIncluded
        - brand
        - connected
        - lifecycle
        - health
        - range
        - coverage
        - totalOccurrences
        - identifiedPageOccurrences
        - redirectOccurrences
        - notFoundOccurrences
        - otherErrorOccurrences
        - buckets
        - crawlerStats
        - pages
        - pagination
      properties:
        success:
          type: boolean
          example: true
        rawEventsAvailable:
          type: boolean
          enum: [false]
          description: Raw server log events are not retained or exposed.
        humanTrafficIncluded:
          type: boolean
          enum: [false]
          description: Human traffic is excluded before analytical persistence.
        brand:
          $ref: '#/components/schemas/BrandSummary'
        connected:
          type: boolean
          description: Whether the crawler integration is currently active with an active key.
        lifecycle:
          type: string
          enum: [PENDING, ACTIVE, PAUSED, DISABLED]
          nullable: true
        health:
          type: string
          enum:
            [
              SETUP_REQUIRED,
              HEALTHY,
              DEGRADED,
              SILENT_UNVERIFIED,
              STALE,
              ERROR,
              DISABLED,
            ]
          nullable: true
        range:
          $ref: '#/components/schemas/CrawlerAnalyticsRange'
        coverage:
          $ref: '#/components/schemas/CrawlerAnalyticsCoverage'
        totalOccurrences:
          type: integer
          format: int64
          minimum: 0
          nullable: true
          description: Observed AI crawler occurrences, or null when the period was not measured.
        identifiedPageOccurrences:
          type: integer
          format: int64
          minimum: 0
          description: Requests linked to a page identity in the retained period.
        redirectOccurrences:
          type: integer
          format: int64
          minimum: 0
        notFoundOccurrences:
          type: integer
          format: int64
          minimum: 0
        otherErrorOccurrences:
          type: integer
          format: int64
          minimum: 0
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/CrawlerAnalyticsBucket'
        crawlerStats:
          type: array
          items:
            $ref: '#/components/schemas/CrawlerAnalyticsCrawler'
        pages:
          type: array
          description: Current page of the listed pages (active in the range unless activeOnly=false), ordered by period volume.
          items:
            $ref: '#/components/schemas/CrawlerAnalyticsPage'
        pagination:
          $ref: '#/components/schemas/CrawlerAnalyticsPagination'

    CrawlerAnalyticsRange:
      type: object
      required:
        - startDate
        - requestedEndDate
        - endDate
        - elapsedBucketCount
        - observedBucketCount
        - verifiedBucketCount
        - rangeTruncated
        - asOf
      properties:
        requestedEndDate:
          type: string
          format: date
        startDate:
          type: string
          format: date
          description: Effective retained start date.
        endDate:
          type: string
          format: date
          description: Effective retained end date.
        elapsedBucketCount:
          type: integer
          minimum: 0
        observedBucketCount:
          type: integer
          minimum: 0
        verifiedBucketCount:
          type: integer
          minimum: 0
        rangeTruncated:
          type: boolean
        asOf:
          type: string
          format: date-time

    CrawlerAnalyticsCoverage:
      type: object
      required:
        - hasObservations
        - dataStatus
        - ingestCoverageFlags
        - countPrecision
        - deliveryContinuity
      properties:
        hasObservations:
          type: boolean
        dataStatus:
          type: string
          enum: [observed, not_measured, incomplete_ingestion]
        ingestCoverageFlags:
          type: integer
          minimum: 0
          description: Bit mask describing delivery, provider, or technical-limit caveats.
        countPrecision:
          type: string
          enum: [exact, at_least_once, capped]
        deliveryContinuity:
          type: string
          enum: [verified, unverified, incomplete]

    CrawlerAnalyticsBucket:
      type: object
      required:
        - bucketStart
        - totalOccurrences
        - crawlerOccurrences
        - redirectOccurrences
        - notFoundOccurrences
        - otherErrorOccurrences
        - dataStatus
        - deliveryContinuity
        - ingestCoverageFlags
      properties:
        bucketStart:
          type: string
          format: date
        totalOccurrences:
          type: integer
          format: int64
          minimum: 0
          nullable: true
        crawlerOccurrences:
          type: object
          additionalProperties:
            type: integer
            format: int64
            minimum: 0
        redirectOccurrences: { type: integer, format: int64, minimum: 0 }
        notFoundOccurrences: { type: integer, format: int64, minimum: 0 }
        otherErrorOccurrences: { type: integer, format: int64, minimum: 0 }
        dataStatus:
          type: string
          enum: [observed, not_measured, incomplete_ingestion]
        deliveryContinuity:
          type: string
          enum: [verified, unverified, incomplete]
        ingestCoverageFlags:
          type: integer
          minimum: 0

    CrawlerAnalyticsCrawler:
      type: object
      required:
        [
          crawlerId,
          name,
          displayName,
          totalOccurrences,
          redirectOccurrences,
          notFoundOccurrences,
          otherErrorOccurrences,
          botType,
        ]
      properties:
        crawlerId:
          type: integer
          minimum: 1
        name:
          type: string
        displayName:
          type: string
        totalOccurrences:
          type: integer
          format: int64
          minimum: 0
        redirectOccurrences: { type: integer, format: int64, minimum: 0 }
        notFoundOccurrences: { type: integer, format: int64, minimum: 0 }
        otherErrorOccurrences: { type: integer, format: int64, minimum: 0 }
        botType:
          type: string
          enum: [training, search, userQuery, agent, other]

    CrawlerAnalyticsPage:
      type: object
      required:
        - pathId
        - path
        - isPseudonymized
        - firstSeenAt
        - lastSeenAt
        - lifetimeCount
        - states
        - occurrences
        - crawlerOccurrences
        - uniqueCrawlers
        - statusGroups
        - crawlerTypes
      properties:
        pathId:
          type: string
          description: Stable page identifier for the integration.
        path:
          type: string
          description: Normalized path without query string or fragment.
        isPseudonymized:
          type: boolean
          description: Whether the displayed path is an internal pseudonym and not a reconnectable URL.
        firstSeenAt:
          type: string
          format: date-time
        lastSeenAt:
          type: string
          format: date-time
        lifetimeCount:
          type: integer
          format: int64
          minimum: 0
        states:
          type: array
          items:
            type: string
            enum: [new, hot, neglected]
        occurrences:
          type: integer
          format: int64
          minimum: 0
        crawlerOccurrences:
          type: object
          additionalProperties:
            type: integer
            format: int64
            minimum: 0
        uniqueCrawlers:
          type: integer
          minimum: 0
        statusGroups:
          $ref: '#/components/schemas/CrawlerAnalyticsStatusGroups'
        crawlerTypes:
          $ref: '#/components/schemas/CrawlerAnalyticsTypeGroups'

    CrawlerAnalyticsStatusGroups:
      type: object
      required:
        [
          success2xx,
          redirect3xx,
          unauthorized401,
          forbidden403,
          notFound404,
          rateLimited429,
          other4xx,
          server5xx,
          otherStatus,
          invalidStatus,
        ]
      properties:
        success2xx: { type: integer, format: int64, minimum: 0 }
        redirect3xx: { type: integer, format: int64, minimum: 0 }
        unauthorized401: { type: integer, format: int64, minimum: 0 }
        forbidden403: { type: integer, format: int64, minimum: 0 }
        notFound404: { type: integer, format: int64, minimum: 0 }
        rateLimited429: { type: integer, format: int64, minimum: 0 }
        other4xx: { type: integer, format: int64, minimum: 0 }
        server5xx: { type: integer, format: int64, minimum: 0 }
        otherStatus: { type: integer, format: int64, minimum: 0 }
        invalidStatus: { type: integer, format: int64, minimum: 0 }

    CrawlerAnalyticsTypeGroups:
      type: object
      required: [liveSearch, training, other]
      properties:
        liveSearch: { type: integer, format: int64, minimum: 0 }
        training: { type: integer, format: int64, minimum: 0 }
        other: { type: integer, format: int64, minimum: 0 }

    CrawlerAnalyticsPagination:
      type: object
      required: [page, limit, totalItems, totalPages, hasMore]
      properties:
        page:
          type: integer
          minimum: 1
        limit:
          type: integer
          minimum: 1
          maximum: 100
        totalItems:
          type: integer
          minimum: 0
        totalPages:
          type: integer
          minimum: 0
        hasMore:
          type: boolean

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code
            message:
              type: string
              description: Human-readable error message

    AuthenticationError:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          example: Unauthorized
        message:
          type: string
          example: Invalid API token or insufficient subscription plan (Growth+ required)

    InternalServerError:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
        message:
          type: string

    RateLimitError:
      type: object
      required: [error, limit, message, retryAfter]
      properties:
        error:
          type: string
          example: Too Many Requests
        limit:
          type: string
          enum: [burst, daily]
        message:
          type: string
        retryAfter:
          type: integer
          minimum: 1

  responses:
    InternalServerError:
      description: An unexpected server error occurred
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/InternalServerError'
          example:
            error: Internal Server Error
            message: An unexpected error occurred
    Unauthorized:
      description: Authentication failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AuthenticationError'
          example:
            error: Unauthorized
            message: Invalid API token or insufficient subscription plan (Growth+ required)
    InvalidParameter:
      description: Invalid request parameter
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_PARAMETER
              message: 'limit: Maximum limit is 100'
    InsufficientPlan:
      description: The authenticated team is not entitled to Crawler Analytics
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INSUFFICIENT_PLAN
              message: Crawler Analytics requires an active Business plan or higher
    RateLimitExceeded:
      description: Per-minute or daily team rate limit exceeded
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
        Retry-After:
          description: Seconds before retrying, based on the actual blocking burst or daily window
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
          example:
            error: Too Many Requests
            limit: burst
            message: Per-minute rate limit (1000/min) exceeded. Please try again in 12 seconds.
            retryAfter: 12
    BrandNotFound:
      description: Brand not found
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: BRAND_NOT_FOUND
              message: Brand not found or not accessible
    CompetitorNotFound:
      description: Brand not accessible (BRAND_NOT_FOUND) or competitor not found (COMPETITOR_NOT_FOUND)
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: COMPETITOR_NOT_FOUND
              message: Competitor not found
    SourceNotFound:
      description: Brand not accessible (BRAND_NOT_FOUND) or source not found (SOURCE_NOT_FOUND)
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: SOURCE_NOT_FOUND
              message: Source not found
    PromptNotFound:
      description: Brand not accessible (BRAND_NOT_FOUND) or prompt not found (PROMPT_NOT_FOUND)
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: PROMPT_NOT_FOUND
              message: Prompt not found
    AnswerNotFound:
      description: Brand not accessible (BRAND_NOT_FOUND) or answer not found (ANSWER_NOT_FOUND)
      headers:
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitBurstLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitBurstRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitBurstReset'
        X-RateLimit-Daily-Limit:
          $ref: '#/components/headers/RateLimitDailyLimit'
        X-RateLimit-Daily-Remaining:
          $ref: '#/components/headers/RateLimitDailyRemaining'
        X-RateLimit-Daily-Reset:
          $ref: '#/components/headers/RateLimitDailyReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ANSWER_NOT_FOUND
              message: Answer not found
