openapi: 3.1.0
info:
  title: FullMention Public API
  version: 1.0.0
  description: Snapshot-first API for AI recommendation intelligence.
  license:
    name: Proprietary
    url: https://fullmention.com/terms
servers:
  - url: https://api.fullmention.com/v1
security:
  - bearerAuth: []
tags:
  - name: System
    description: System health and diagnostics
  - name: Keywords
    description: Manage keywords to track across engines
  - name: Tags
    description: Manage organizational tags
  - name: Runs
    description: Orchestrate execution and reservations
  - name: Results
    description: Access result snapshots and fanout data
  - name: Webhooks
    description: Configure asynchronous delivery of events
paths:
  /health:
    get:
      tags: [ System ]
      summary: Health check
      security: []
      responses:
        '200':
          description: Service health status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: healthCheck
  /status:
    get:
      tags: [ System ]
      summary: Get system and search engine status
      security: []
      responses:
        '200':
          description: Dynamic system and engine status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: systemStatus
  /engines/status:
    get:
      tags: [ System ]
      summary: Get engine status (alias for /status)
      security: []
      responses:
        '200':
          description: Dynamic system and engine status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: enginesStatus
  /keywords:
    get:
      tags: [ Keywords ]
      summary: List keywords
      parameters:
        - $ref: '#/components/parameters/Tags'
        - $ref: '#/components/parameters/TagMode'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Language'
        - $ref: '#/components/parameters/Location'
        - $ref: '#/components/parameters/Engine'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Keyword list
          headers:
            X-Quota-Limit:
              $ref: '#/components/headers/XQuotaLimit'
            X-Quota-Used:
              $ref: '#/components/headers/XQuotaUsed'
            X-Quota-Extra:
              $ref: '#/components/headers/XQuotaExtra'
            X-Quota-Remaining:
              $ref: '#/components/headers/XQuotaRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeywordListResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: listKeywords
    post:
      tags: [ Keywords ]
      summary: Create keywords
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateKeywordsRequest'
      responses:
        '201':
          description: Keywords created or returned idempotently
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateKeywordsResponse'
        '422':
          $ref: '#/components/responses/ValidationError'
      operationId: createKeywords
  /quota:
    get:
      tags: [ Account ]
      summary: Get available quota
      responses:
        '200':
          description: Quota successfully retrieved
          headers:
            X-Quota-Remaining:
              $ref: '#/components/headers/XQuotaRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuotaResponse'
        '404':
          $ref: "#/components/responses/NotFoundError"
      operationId: getQuota
  /keywords/{keywordId}:
    patch:
      tags: [ Keywords ]
      summary: Update keyword
      parameters:
        - $ref: '#/components/parameters/KeywordId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateKeywordRequest'
      responses:
        '200':
          description: Updated keyword
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeywordResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          $ref: '#/components/responses/ValidationError'
      operationId: updateKeyword
    delete:
      tags: [ Keywords ]
      summary: Soft delete keyword
      parameters:
        - $ref: '#/components/parameters/KeywordId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Deleted keyword
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteResponse'
        "400":
          $ref: "#/components/responses/ValidationError"
      operationId: softDeleteKeyword
  /tags:
    get:
      tags: [ Tags ]
      summary: List tags
      parameters:
        - name: prefix
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Tag list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagListResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: listTags
  /runs/estimate:
    post:
      tags: [ Runs ]
      summary: Estimate run cost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunScopeRequest'
      responses:
        '200':
          description: Run estimate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunEstimateResponse'
        "400":
          $ref: "#/components/responses/ValidationError"
      operationId: estimateRunCost
  /runs:
    get:
      tags: [ Runs ]
      summary: List runs
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/RunStatus'
        - $ref: '#/components/parameters/Tags'
        - $ref: '#/components/parameters/TagMode'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Run list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunListResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: listRuns
    post:
      tags: [ Runs ]
      summary: Trigger run
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunScopeRequest'
      responses:
        '201':
          description: Run queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunResponse'
        '409':
          $ref: '#/components/responses/ConflictError'
      operationId: triggerRun
  /runs/{runId}:
    get:
      tags: [ Runs ]
      summary: Get run
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          $ref: '#/components/responses/ValidationError'
      operationId: getRun
    delete:
      tags: [ Runs ]
      summary: Cancel or abort an active batch run
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Cancelled run details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '400':
          $ref: '#/components/responses/ValidationError'
      operationId: cancelRun
  /results/latest:
    get:
      tags: [ Results ]
      summary: Get latest results
      description: "`country` and `language` filters use stored codes, for example `country=DK&language=da`."
      parameters:
        - $ref: '#/components/parameters/Tags'
        - $ref: '#/components/parameters/TagMode'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Language'
        - $ref: '#/components/parameters/Location'
        - $ref: '#/components/parameters/Engine'
        - name: keywordId
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Latest result snapshots
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResultListResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: getLatestResults
  /results/{resultId}/fanout-sources:
    get:
      tags: [ Results ]
      summary: List full fanout sources for a result
      description: Returns paginated normalized fanout sources. Use this endpoint
        instead of expecting full fanout inline on latest result snapshots. Returns
        `422 fanout_not_enabled` when fanout was not requested for the result.
      parameters:
        - $ref: '#/components/parameters/ResultId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Fanout source list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FanoutSourceListResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          $ref: '#/components/responses/ValidationError'
      operationId: listFullFanoutSourcesForAResult
  /webhooks:
    get:
      tags: [ Webhooks ]
      summary: List webhooks
      responses:
        '200':
          description: Webhook list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookListResponse'
        "404":
          $ref: "#/components/responses/NotFoundError"
      operationId: listWebhooks
    post:
      tags: [ Webhooks ]
      summary: Create webhook
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Created webhook
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedWebhookResponse'
        "400":
          $ref: "#/components/responses/ValidationError"
      operationId: createWebhook
  /webhooks/{webhookId}:
    patch:
      tags: [ Webhooks ]
      summary: Update webhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
      responses:
        '200':
          description: Updated webhook
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        "400":
          $ref: "#/components/responses/ValidationError"
      operationId: updateWebhook
    delete:
      tags: [ Webhooks ]
      summary: Delete webhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Deleted webhook
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteResponse'
        "400":
          $ref: "#/components/responses/ValidationError"
      operationId: deleteWebhook
  /webhooks/{webhookId}/test:
    post:
      tags: [ Webhooks ]
      summary: Send test webhook event
      parameters:
        - $ref: '#/components/parameters/WebhookId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Test webhook queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
      operationId: sendTestWebhookEvent
  /webhooks/{webhookId}/rotate-secret:
    post:
      tags: [ Webhooks ]
      summary: Rotate webhook signing secret
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Pending signing secret created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotatedWebhookSecretResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
      operationId: rotateWebhookSigningSecret
  /webhooks/{webhookId}/promote-secret:
    post:
      tags: [ Webhooks ]
      summary: Promote pending webhook signing secret
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Pending signing secret promoted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '404':
          $ref: '#/components/responses/NotFoundError'
      operationId: promoteWebhookSigningSecret
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  headers:
    XQuotaLimit:
      schema:
        type: integer
    XQuotaUsed:
      schema:
        type: integer
    XQuotaExtra:
      schema:
        type: integer
    XQuotaRemaining:
      schema:
        type: integer
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 200
    KeywordId:
      name: keywordId
      in: path
      required: true
      schema:
        type: string
    RunId:
      name: runId
      in: path
      required: true
      schema:
        type: string
    ResultId:
      name: resultId
      in: path
      required: true
      schema:
        type: string
    WebhookId:
      name: webhookId
      in: path
      required: true
      schema:
        type: string
    Tags:
      name: tags
      in: query
      schema:
        type: string
      description: Comma-separated tags.
    TagMode:
      name: tagMode
      in: query
      schema:
        type: string
        enum: [ and, or ]
        default: or
    Country:
      name: country
      in: query
      schema:
        type: string
    Language:
      name: language
      in: query
      schema:
        type: string
    Location:
      name: location
      in: query
      schema:
        type: string
    Engine:
      name: engine
      in: query
      schema:
        $ref: '#/components/schemas/Engine'
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 500
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
  responses:
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnauthorizedError:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ForbiddenError:
      description: API key does not have access
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ConflictError:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    TooManyRequestsError:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    HealthResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required: [ status, service, environment, region, version ]
          properties:
            status:
              type: string
              enum: [ ok ]
            service:
              type: string
            environment:
              type: string
              enum: [ local, production, test ]
            region:
              type: string
            version:
              type: string
        meta:
          type: object
          required: [ requestId, timestamp ]
          properties:
            requestId:
              type: string
            timestamp:
              type: string
              format: date-time
    EngineStatus:
      type: object
      required: [ status, message ]
      properties:
        status:
          type: string
          enum: [ operational, degraded, major_outage ]
        message:
          type: string
    StatusResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required: [ status, services, engines ]
          properties:
            status:
              type: string
              enum: [ operational, degraded, major_outage ]
            services:
              type: object
              required: [ api, worker ]
              properties:
                api:
                  type: string
                  enum: [ operational ]
                worker:
                  type: string
                  enum: [ operational ]
            engines:
              type: object
              required: [ openai, openaiMini, gemini ]
              properties:
                openai:
                  $ref: '#/components/schemas/EngineStatus'
                openaiMini:
                  $ref: '#/components/schemas/EngineStatus'
                gemini:
                  $ref: '#/components/schemas/EngineStatus'
        meta:
          type: object
          required: [ requestId, timestamp ]
          properties:
            requestId:
              type: string
            timestamp:
              type: string
              format: date-time
    Engine:
      type: string
      enum: [ openai, openai-mini, gemini ]
      description: Public engine label. `gemini` uses Gemini 3.5 Flash, `openai` uses GPT-5, and `openai-mini` uses GPT-5-mini.
    RunStatus:
      type: string
      enum: [ queued, submitted, processing, success, failed ]
    TagMode:
      type: string
      enum: [ and, or ]
    PaginationMeta:
      type: object
      required: [ nextCursor ]
      properties:
        nextCursor:
          type: [ string, 'null' ]
    Keyword:
      type: object
      required:
        [
          id,
          keyword,
          country,
          countryCode,
          language,
          languageCode,
          engines,
          tags,
          active,
          createdAt,
          updatedAt
        ]
      properties:
        id:
          type: string
        keyword:
          type: string
          maxLength: 200
        country:
          type: string
        countryCode:
          type: string
        language:
          type: string
        languageCode:
          type: string
        location:
          type: [ string, 'null' ]
          maxLength: 120
        engines:
          type: array
          maxItems: 3
          items:
            $ref: '#/components/schemas/Engine'
        tags:
          type: array
          maxItems: 25
          items:
            type: string
            maxLength: 80
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    KeywordInput:
      type: object
      required: [ keyword, country, countryCode, language, languageCode ]
      properties:
        keyword:
          type: string
          maxLength: 200
        country:
          type: string
        countryCode:
          type: string
          minLength: 2
          maxLength: 3
        language:
          type: string
        languageCode:
          type: string
          minLength: 2
          maxLength: 5
        location:
          type: string
          maxLength: 120
        engines:
          type: array
          default: [ openai-mini ]
          description: Defaults to `openai-mini`. Supports `openai`, `openai-mini`, and `gemini` for parallel run execution.
          minItems: 1
          maxItems: 3
          items:
            $ref: '#/components/schemas/Engine'
        tags:
          type: array
          maxItems: 25
          items:
            type: string
            maxLength: 80
    CreateKeywordsRequest:
      type: object
      required: [ keywords ]
      properties:
        keywords:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/KeywordInput'
    UpdateKeywordRequest:
      type: object
      properties:
        keyword:
          type: string
          maxLength: 200
        country:
          type: string
        countryCode:
          type: string
          minLength: 2
          maxLength: 3
        language:
          type: string
        languageCode:
          type: string
          minLength: 2
          maxLength: 5
        location:
          type: [ string, 'null' ]
          maxLength: 120
        engines:
          type: array
          minItems: 1
          maxItems: 3
          items:
            $ref: '#/components/schemas/Engine'
        tags:
          type: array
          maxItems: 25
          items:
            type: string
            maxLength: 80
        active:
          type: boolean
    KeywordListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Keyword'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    KeywordResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          $ref: '#/components/schemas/Keyword'
        meta:
          type: object
    CreateKeywordsResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/Keyword'
              - type: object
                properties:
                  created:
                    type: boolean
        meta:
          type: object
          properties:
            createdCount:
              type: integer
            existingCount:
              type: integer
    TagListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            type: object
            required: [ tag, keywordCount ]
            properties:
              tag:
                type: string
              keywordCount:
                type: integer
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    QuotaResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required:
            [
              remaining,
              monthlyLimit,
              used,
              reservedMax,
              extra,
              periodStart,
              periodEnd
            ]
          properties:
            remaining:
              type: integer
            monthlyLimit:
              type: integer
            used:
              type: integer
            reservedMax:
              type: integer
            extra:
              type: integer
            periodStart:
              type: string
              format: date-time
            periodEnd:
              type: string
              format: date-time
        meta:
          type: object
    RunScopeRequest:
      type: object
      properties:
        tags:
          type: array
          items:
            type: string
        tagMode:
          $ref: '#/components/schemas/TagMode'
          default: or
        options:
          type: object
          properties:
            fanout:
              type: boolean
              default: false
              description: Enable OpenAI/OpenAI-mini fanout add-on. Costs 9 additional credits for openai (GPT-5) and 1 additional credit for openai-mini (GPT-5-mini) per successful execution that produces fanout data.
    FailureSummary:
      type: object
      properties:
        failedEngineExecutions:
          type: integer
        emptyEngineExecutions:
          type: integer
        delayedEngineExecutions:
          type: integer
    Run:
      type: object
      required:
        [
          id,
          status,
          logicalKeywordCount,
          engineExecutionCount,
          fanoutExecutionCount,
          estimatedBaseCreditCost,
          estimatedFanoutCreditCost,
          estimatedMaxCreditCost,
          executionMode,
          createdAt,
          startedAt,
          completedAt
        ]
      properties:
        id:
          type: string
        status:
          $ref: '#/components/schemas/RunStatus'
        scope:
          type: object
          properties:
            tags:
              type: array
              items:
                type: string
            tagMode:
              $ref: '#/components/schemas/TagMode'
              default: or
            options:
              type: object
              properties:
                fanout:
                  type: boolean
        logicalKeywordCount:
          type: integer
          maximum: 1000
        engineExecutionCount:
          type: integer
          maximum: 2000
        fanoutExecutionCount:
          type: integer
        estimatedBaseCreditCost:
          type: integer
        estimatedFanoutCreditCost:
          type: integer
        estimatedMaxCreditCost:
          type: integer
        resultCount:
          type: integer
        executionMode:
          type: string
          enum: [ batch, demo ]
        failureSummary:
          anyOf:
            - $ref: '#/components/schemas/FailureSummary'
            - type: 'null'
        createdAt:
          type: string
          format: date-time
        startedAt:
          type: [ string, 'null' ]
          format: date-time
        completedAt:
          type: [ string, 'null' ]
          format: date-time
        baseCreditsCaptured:
          type: integer
          minimum: 0
        fanoutCreditsCaptured:
          type: integer
          minimum: 0
        reservedCreditsReleased:
          type: integer
          minimum: 0
        progress:
          anyOf:
            - type: object
              required: [ status, totalRequests, completedRequests, failedRequests, percentage ]
              properties:
                status:
                  type: string
                  enum: [ queued, processing, success, failed ]
                  description: Unified execution progress status.
                totalRequests:
                  type: integer
                  description: Total underlying provider requests to execute.
                completedRequests:
                  type: integer
                  description: Successfully completed provider requests.
                failedRequests:
                  type: integer
                  description: Failed provider requests.
                percentage:
                  type: integer
                  minimum: 0
                  maximum: 100
                  description: Completion percentage.
            - type: 'null'
          description: Live execution and request progress details.
    RunResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          $ref: '#/components/schemas/Run'
        meta:
          type: object
    RunListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Run'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    RunEstimateResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required:
            [
              logicalKeywordCount,
              engineExecutionCount,
              fanoutExecutionCount,
              estimatedBaseCreditCost,
              estimatedFanoutCreditCost,
              estimatedMaxCreditCost,
              quotaRemaining,
              canRun
            ]
          properties:
            logicalKeywordCount:
              type: integer
            engineExecutionCount:
              type: integer
            fanoutExecutionCount:
              type: integer
            estimatedBaseCreditCost:
              type: integer
            estimatedFanoutCreditCost:
              type: integer
            estimatedMaxCreditCost:
              type: integer
            quotaRemaining:
              type: integer
            canRun:
              type: boolean
        meta:
          type: object
    BrandRanking:
      type: object
      required: [ position, name ]
      properties:
        position:
          type: integer
          minimum: 1
        name:
          type: string
    WebsiteRanking:
      type: object
      required: [ position, domain ]
      properties:
        position:
          type: integer
          minimum: 1
        domain:
          type: string
    ProductRanking:
      type: object
      required: [ position, name ]
      properties:
        position:
          type: integer
          minimum: 1
        name:
          type: string
    FanoutSource:
      type: object
      required: [ rank, url, domain, title, query, sourceType ]
      properties:
        id:
          type: string
          description: Present on full fanout endpoint rows.
        rank:
          type: integer
          minimum: 1
        url:
          type: string
          maxLength: 1000
        domain:
          type: string
          maxLength: 200
        title:
          type: [ string, 'null' ]
          maxLength: 300
        query:
          type: [ string, 'null' ]
          maxLength: 300
        sourceType:
          type: string
          enum: [ web_search ]
    Fanout:
      type: object
      required: [ queryCount, totalSourcesFound, sources ]
      properties:
        queryCount:
          type: integer
          minimum: 0
        totalSourcesFound:
          type: integer
          minimum: 0
        sources:
          type: array
          maxItems: 25
          items:
            $ref: '#/components/schemas/FanoutSource'
    Result:
      type: object
      required:
        [
          id,
          keywordId,
          keyword,
          country,
          countryCode,
          language,
          languageCode,
          engine,
          runId,
          methodVersion,
          tags,
          description,
          categorySuggestions,
          brandRankings,
          websiteRankings,
          productRankings,
          metrics,
          updatedAt
        ]
      properties:
        id:
          type: string
        keywordId:
          type: string
        keyword:
          type: string
        country:
          type: string
        countryCode:
          type: string
        language:
          type: string
        languageCode:
          type: string
        location:
          type: [ string, 'null' ]
        engine:
          $ref: '#/components/schemas/Engine'
        runId:
          type: string
        methodVersion:
          type: string
          example: rec_snapshot_v3
        tags:
          type: array
          items:
            type: string
        description:
          type: string
        categorySuggestions:
          type: array
          maxItems: 10
          items:
            type: string
        brandRankings:
          type: array
          maxItems: 25
          items:
            $ref: '#/components/schemas/BrandRanking'
        websiteRankings:
          type: array
          maxItems: 25
          items:
            $ref: '#/components/schemas/WebsiteRanking'
        productRankings:
          type: array
          maxItems: 25
          items:
            $ref: '#/components/schemas/ProductRanking'
        fanout:
          $ref: '#/components/schemas/Fanout'
        metrics:
          type: object
          properties:
            brandCount:
              type: integer
            websiteCount:
              type: integer
            productCount:
              type: integer
            fanoutSourceCount:
              type: integer
            fanoutQueryCount:
              type: integer
        truncated:
          type: object
          properties:
            brandRankings:
              type: boolean
            websiteRankings:
              type: boolean
            productRankings:
              type: boolean
            fanoutSourcesPreview:
              type: boolean
        updatedAt:
          type: string
          format: date-time
    ResultListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Result'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    FanoutSourceListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/FanoutSource'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    WebhookEventType:
      type: string
      enum: [ run.completed, run.failed, quota.low ]
    Webhook:
      type: object
      required: [ id, url, events, active, createdAt, updatedAt ]
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateWebhookRequest:
      type: object
      required: [ url, events ]
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventType'
    UpdateWebhookRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/WebhookEventType'
        active:
          type: boolean
    WebhookResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          $ref: '#/components/schemas/Webhook'
        meta:
          type: object
    CreatedWebhookResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Webhook'
            - type: object
              required: [ signingSecret ]
              properties:
                signingSecret:
                  type: string
                  description: Raw signing secret. Shown only once at creation.
        meta:
          type: object
    RotatedWebhookSecretResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Webhook'
            - type: object
              required: [ pendingSigningSecret ]
              properties:
                pendingSigningSecret:
                  type: string
                  description: Raw pending signing secret. Shown only once during rotation.
        meta:
          type: object
    WebhookListResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Webhook'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    WebhookTestResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required: [ queued ]
          properties:
            queued:
              type: boolean
        meta:
          type: object
    DeleteResponse:
      type: object
      required: [ data, meta ]
      properties:
        data:
          type: object
          required: [ deleted ]
          properties:
            id:
              type: string
            deleted:
              type: boolean
        meta:
          type: object
    ErrorResponse:
      type: object
      required: [ error ]
      properties:
        error:
          type: object
          required: [ code, message, requestId ]
          properties:
            code:
              type: string
              enum:
                - invalid_payload
                - invalid_cursor
                - keyword_not_found
                - run_not_found
                - run_already_processing
                - quota_exceeded
                - rate_limited
                - engine_unavailable
                - run_scope_too_large
                - result_not_ready
                - validation_failed
                - webhook_not_found
                - webhook_delivery_failed
                - fanout_not_enabled
                - idempotency_key_required
                - idempotency_in_progress
                - idempotency_conflict
                - internal_error
                - unauthorized
                - forbidden
            message:
              type: string
            requestId:
              type: string
            details:
              type: array
              items:
                type: object
