openapi: 3.1.0

# Radarkit public API. Hand written. This file is the source of truth for the API
# Reference tab in the docs. tests/Feature/Api/OpenApiCoverageTest fails when a
# /v1 route is missing here or a path here has no route.

info:
  title: Radarkit API
  version: 1.0.0
  summary: Your AI visibility data, outside the dashboard.
  description: |
    Radarkit tracks how AI assistants answer the questions your customers ask, and what each answer says about your brand. This API gives you that data as JSON.

    Every request needs an API key in the `Authorization` header. Every response has the same shape: `data`, `meta`, `credits` and `request_id`. Most calls cost 1 API credit. Heavier calls cost 2. Exports spend Export rows instead, one per row.
  contact:
    name: Radarkit support
    email: mail@radarkit.ai
    url: https://radarkit.ai

servers:
  - url: https://api.radarkit.ai/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Account
    description: Your key, your plan and both balances. Free.
  - name: Projects
    description: One project is one brand or website. Every other call needs a project id.
  - name: Topics
    description: Topics group prompts. Every prompt belongs to one topic.
  - name: Prompts
    description: The questions Radarkit asks every AI model on a schedule.
  - name: Visibility
    description: The daily visibility score per model, precomputed.
  - name: Responses
    description: One row per AI answer, with everything Radarkit found in it.
  - name: Sources
    description: The web pages AI answers cite or consult.
  - name: Competitors
    description: Who the answers recommend and how often.
  - name: Rankings
    description: Recommended domains with their position in the answer.
  - name: Query fanout
    description: The web searches a model ran before answering, one row per answer.
  - name: Web search
    description: How often the models searched and what they searched for, summarised.
  - name: Sentiment
    description: How the answers talk about your brand.
  - name: Exports
    description: |
      Thousands of rows as one gzip file. Four calls in order: estimate, create, poll, download.

      Estimate is free and returns the exact row count. Create charges that many Export rows and queues the job. Poll the export until its status is `ready`. Download follows a redirect to a signed link that works for 30 minutes. Files are kept for 7 days.
  - name: Content
    description: Have the Content agent write an article for a prompt, then poll the run.

paths:
  /me:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Get account
      description: |
        Who the key belongs to, what it may do, and how many API credits and Export rows are left.

        Use it: to check a new key, or to read balances before a batch of calls.

        Cost: Free.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: The key, the plan and both balances.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Account"
                  meta:
                    type: object
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  user: { id: 1, name: "Ada Lovelace", email: "ada@example.com" }
                  key:
                    name: "Reporting"
                    prefix: "rk_live_a8Fk2mQ1"
                    scopes: ["read"]
                    restricted_to_projects: false
                    expires_at: null
                  plan: { name: "growth", display_name: "Growth", status: "active", renews_at: "2026-10-01T00:00:00+00:00" }
                  credits: { available: 2950, plan: 2950, additional: 0, plan_limit: 3000 }
                  export_rows: { available: 30000, plan: 30000, additional: 0, plan_limit: 30000 }
                  rate_limits: { perMinute: 60, perSecond: 10 }
                meta: {}
                credits: { charged: 0, remaining: null }
                request_id: "req_01j9x4m2p7q8r9s0t1u2v3w4x5"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`PLAN_INACTIVE`: the key owner has no active plan."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`API_FROZEN`: API access for the account is paused. `PLAN_NO_API`: the plan has no API access."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects:
    get:
      tags: [Projects]
      operationId: listProjects
      summary: List projects
      description: |
        Every project the key can reach, with the id you need for every other call.

        Use it: first. Copy the `id` of the project you want.

        Cost: 1 API credit, charged to the key's owner.

        Needs: Read.
      responses:
        "200":
          description: The projects, whole. No paging.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Project"
                  meta:
                    type: object
                    properties:
                      count: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: "64f1a2b3c4d5e6f708192a3b"
                    name: "Acme CRM"
                    url: "https://www.acme.com"
                    region: "US"
                    timezone: "America/New_York"
                    models: ["chatgpt", "gemini", "perplexity", "copilot"]
                    prompts_count: 48
                    next_run_at: "2026-09-04T03:00:00+00:00"
                    created_at: "2026-05-12T09:30:00+00:00"
                meta: { count: 1 }
                credits: { charged: 1, remaining: 2949 }
                request_id: "req_01j9x4n8k3l4m5n6o7p8q9r0s1"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/ScopeMissing"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}:
    get:
      tags: [Projects]
      operationId: getProject
      summary: Get project
      description: |
        One project: name, URL, region, timezone, the models it tracks, how many prompts it has and when it runs next.

        Cost: 1 API credit.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
      responses:
        "200":
          description: The project.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
                  meta:
                    type: object
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: "64f1a2b3c4d5e6f708192a3b"
                  name: "Acme CRM"
                  url: "https://www.acme.com"
                  region: "US"
                  timezone: "America/New_York"
                  models: ["chatgpt", "gemini", "perplexity", "copilot"]
                  prompts_count: 48
                  next_run_at: "2026-09-04T03:00:00+00:00"
                  created_at: "2026-05-12T09:30:00+00:00"
                meta: {}
                credits: { charged: 1, remaining: 2948 }
                request_id: "req_01j9x4n8k3l4m5n6o7p8q9r0s2"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}/topics:
    get:
      tags: [Topics]
      operationId: listTopics
      summary: List topics
      description: |
        Every active topic in the project with how many prompts it holds. Sorted by name.

        Use it: to find a `topic_id` before adding prompts.

        Cost: 1 API credit.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
      responses:
        "200":
          description: The topics, whole. No paging.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Topic"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      count: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - { id: 17, name: "Pricing", status: "active", prompts_count: 12, created_at: "2026-05-12T09:31:00+00:00" }
                  - { id: 18, name: "Comparisons", status: "active", prompts_count: 36, created_at: "2026-05-12T09:31:00+00:00" }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", count: 2 }
                credits: { charged: 1, remaining: 2947 }
                request_id: "req_01j9x4n8k3l4m5n6o7p8q9r0s3"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Topics]
      operationId: addTopic
      summary: Add topic
      description: |
        Creates a topic. Names are unique within a project and the check ignores letter case.

        If a topic with that name exists you get `409 TOPIC_EXISTS` with the existing id in `details.topic_id`. Use that id. Nothing was created twice.

        Cost: 1 API credit.

        Needs: Write. Your seat must be allowed to manage topics.
      parameters:
        - $ref: "#/components/parameters/project"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: The topic name.
            example:
              name: "Pricing"
      responses:
        "201":
          description: The new topic.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Topic"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data: { id: 17, name: "Pricing", status: "active", created_at: "2026-09-03T10:20:11+00:00" }
                meta: { project: "64f1a2b3c4d5e6f708192a3b" }
                credits: { charged: 1, remaining: 2944 }
                request_id: "req_01j9x4y7m8n9o0p1q2r3s4t5u6"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: "`SCOPE_MISSING`: the key has no Write permission. `PROJECT_FORBIDDEN`: the key cannot reach this project. `PERMISSION_DENIED`: your seat cannot manage topics."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "409":
          description: "`TOPIC_EXISTS`: a topic with that name exists. `details.topic_id` holds its id."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "TOPIC_EXISTS"
                  message: "A topic named \"Pricing\" already exists."
                  details: { topic_id: 17, name: "Pricing" }
                request_id: "req_01j9x4y7m8n9o0p1q2r3s4t5u7"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}/prompts:
    get:
      tags: [Prompts]
      operationId: listPrompts
      summary: List prompts
      description: |
        The active prompts with their topic, when they were last checked and their status per model. Oldest first, so new prompts land at the end.

        Status per model is `pending` before the first run, `processing` during a run, `completed` after it, and `hold` while waiting for the next scheduled run.

        Cost: 1 API credit per page. A page holds up to 500 prompts.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - name: topic_id
          in: query
          description: Only prompts in this topic.
          schema: { type: integer, minimum: 1 }
        - name: limit
          in: query
          description: Prompts per page, 1 to 500. Default 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 500 }
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: One page of prompts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Prompt"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: 2048
                    text: "best crm for a startup"
                    topic_id: 17
                    last_checked_at: "2026-08-31T03:12:40+00:00"
                    created_at: "2026-05-12T09:31:00+00:00"
                    models: { chatgpt: "completed", gemini: "completed", perplexity: "hold" }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", topic_id: 17, models: ["chatgpt", "gemini", "perplexity"], limit: 500, count: 1, has_more: false, next_cursor: null }
                credits: { charged: 1, remaining: 2947 }
                request_id: "req_01j9x4t3c4d5e6f7g8h9i0j1k2"
        "400":
          $ref: "#/components/responses/BadCursor"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Prompts]
      operationId: addPrompts
      summary: Add prompts
      description: |
        Adds up to 50 prompts to one topic. Send the same list twice and nothing doubles: a prompt that already exists in the project is returned under `skipped` with `reason: duplicate` and its existing id. Matching ignores letter case.

        When the plan's prompt limit is reached part way, the prompts before it are still added and the rest come back with `reason: limit`. When nothing fits at all, the call is refused with `402 PROMPT_LIMIT_REACHED` and no credit is charged.

        New prompts run on the project's next scheduled run. To see answers sooner, use Run a prompt now.

        Cost: 1 API credit per call.

        Limits: 50 prompts per call. 5,000 prompts per key per rolling day, REST and MCP together. Each prompt up to 255 characters.

        Needs: Write. Your seat must be allowed to manage prompts.
      parameters:
        - $ref: "#/components/parameters/project"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [topic_id, prompts]
              properties:
                topic_id:
                  type: integer
                  description: The topic the prompts go into. From List topics.
                prompts:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items: { type: string, maxLength: 255 }
                  description: The prompt texts.
            example:
              topic_id: 17
              prompts:
                - "how much does acme crm cost"
                - "acme crm pricing vs hubspot"
                - "is acme crm free for small teams"
      responses:
        "201":
          description: What was added and what was skipped.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      added:
                        type: array
                        items:
                          $ref: "#/components/schemas/Prompt"
                      skipped:
                        type: array
                        items:
                          type: object
                          properties:
                            text: { type: string }
                            reason: { type: string, enum: [duplicate, limit] }
                            prompt_id: { type: [integer, "null"], description: "The existing prompt when the reason is duplicate." }
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      topic_id: { type: integer }
                      added: { type: integer }
                      skipped: { type: integer }
                      limit_reached: { type: boolean }
                      prompts:
                        type: object
                        description: Prompts in use and allowed across all of the owner's projects.
                        properties:
                          used: { type: integer }
                          limit: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  added:
                    - { id: 2101, text: "how much does acme crm cost", topic_id: 17, last_checked_at: null, created_at: "2026-09-03T10:21:05+00:00" }
                    - { id: 2102, text: "is acme crm free for small teams", topic_id: 17, last_checked_at: null, created_at: "2026-09-03T10:21:05+00:00" }
                  skipped:
                    - { text: "acme crm pricing vs hubspot", reason: "duplicate", prompt_id: 2049 }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", topic_id: 17, added: 2, skipped: 1, limit_reached: false, prompts: { used: 50, limit: 300 } }
                credits: { charged: 1, remaining: 2943 }
                request_id: "req_01j9x4z3v4w5x6y7z8a9b0c1d2"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`PROMPT_LIMIT_REACHED`: the plan's prompt limit is used up, `details` has `used` and `limit`. `INSUFFICIENT_CREDITS`: no API credits left. `PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Write permission. `PROJECT_FORBIDDEN`: the key cannot reach this project. `PERMISSION_DENIED`: your seat cannot manage prompts."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`PROJECT_NOT_FOUND`: no project with that id. `TOPIC_NOT_FOUND`: no active topic with that id in this project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          description: "`RATE_LIMITED`: too many calls. `PROMPTS_DAILY_CAP`: this key added its 5,000 prompts for the day. Both carry `Retry-After`."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /projects/{project}/prompts/{prompt}/run:
    post:
      tags: [Prompts]
      operationId: runPromptNow
      summary: Run a prompt now
      description: |
        Asks every model the project tracks this prompt right away, instead of waiting for the next scheduled run. The run happens in the background and takes a few minutes.

        To know when it is done, list prompts and watch the status per model go from `pending` to `processing` to `completed`. Then list responses for the prompt. The new rows have `realtime: true`.

        Cost: 1 API credit plus 1 realtime credit from the project owner's balance. Nothing is charged when the run is refused.

        Limits: 10 per minute per key. One run per prompt at a time.

        Needs: Write.
      parameters:
        - $ref: "#/components/parameters/project"
        - name: prompt
          in: path
          required: true
          description: The prompt id. From List prompts.
          schema: { type: integer }
      responses:
        "202":
          description: The run is queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      prompt:
                        $ref: "#/components/schemas/Prompt"
                      models:
                        type: array
                        items: { type: string }
                        description: The models that will answer.
                      status: { type: string, enum: [queued] }
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      realtime_credits_remaining: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  prompt: { id: 2048, text: "best crm for a startup", topic_id: 17, last_checked_at: null, created_at: "2026-05-12T09:31:00+00:00" }
                  models: ["chatgpt", "gemini", "perplexity"]
                  status: "queued"
                meta: { project: "64f1a2b3c4d5e6f708192a3b", realtime_credits_remaining: 11 }
                credits: { charged: 1, remaining: 2942 }
                request_id: "req_01j9x503e4f5g6h7i8j9k0l1m2"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`INSUFFICIENT_REALTIME_CREDITS`: the owner has no realtime credits. `INSUFFICIENT_CREDITS`: no API credits left. `PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: "`PROJECT_NOT_FOUND`: no project with that id. `PROMPT_NOT_FOUND`: no active prompt with that id in this project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: "`PROMPT_PROCESSING`: a run for this prompt is already going. Wait for it to finish."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: "`NO_MODELS`: the owner's plan has no AI models."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}/visibility:
    get:
      tags: [Visibility]
      operationId: getVisibility
      summary: Get visibility
      description: |
        The daily visibility score and brand score per model, with position and sentiment counts for each day, plus the average over the range.

        `summary` is the average across all models. `by_model` has one average per model. `series` has one row per model per day.

        Cost: 1 API credit.

        Limits: Up to 366 days in one call.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
      responses:
        "200":
          description: The scores.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      summary:
                        $ref: "#/components/schemas/VisibilityAverage"
                      by_model:
                        type: object
                        additionalProperties:
                          $ref: "#/components/schemas/VisibilityAverage"
                      series:
                        type: array
                        items:
                          $ref: "#/components/schemas/VisibilityDay"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                      models: { type: array, items: { type: string } }
                      days: { type: integer, description: "Rows in the series." }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  summary: { visibility: 41.3, brand_score: 58.0, days: 7 }
                  by_model:
                    chatgpt: { visibility: 52.1, brand_score: 63.4, days: 7 }
                    gemini: { visibility: 30.5, brand_score: 52.6, days: 7 }
                  series:
                    - date: "2026-08-25"
                      model: "chatgpt"
                      visibility: 50.0
                      brand_score: 61.2
                      positions: { top: 9, middle: 8, bottom: 7 }
                      sentiment: { positive: 17, mixed: 5, negative: 2 }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-25", to: "2026-08-31", models: ["chatgpt", "gemini"], days: 14 }
                credits: { charged: 1, remaining: 2948 }
                request_id: "req_01j9x4p1a2b3c4d5e6f7g8h9i0"
        "400":
          description: "`DATE_RANGE_TOO_LARGE`: more than 366 days."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/responses:
    get:
      tags: [Responses]
      operationId: listResponses
      summary: List responses
      description: |
        One row per AI answer: which prompt, which model, which day, whether your brand was mentioned, where, in what tone, whether the model searched the web, and how many competitors it named.

        Add `include=content` to get the answer text as Markdown in `content_markdown`. It is the same rendering the prompt page shows: headings, lists, your brand in bold, citations as `[1]` and a numbered source list at the end. Never raw HTML.

        Newest first. Send `next_cursor` back as `cursor` with the same filters to get the next page.

        Cost: 1 API credit per page. 2 with `include=content`.

        Limits: Up to 31 days in one call. Up to 100 rows per page. For a year of rows, create an export.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/prompt_id"
        - name: brand_mentioned
          in: query
          description: "`true` for answers that mention your brand, `false` for the ones that missed it."
          schema: { type: boolean }
        - name: include
          in: query
          description: "`content` adds the answer as Markdown. Costs 2 credits instead of 1."
          schema: { type: string, enum: [content] }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: One page of answers.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Response"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: 918273
                    prompt: { id: 2048, text: "best crm for a startup" }
                    model: "chatgpt"
                    date: "2026-08-31"
                    collected_at: "2026-08-31T03:12:40+00:00"
                    brand_mentioned: true
                    position: "top"
                    sentiment: "positive"
                    search_performed: true
                    search_queries: ["best crm startups 2026", "acme crm pricing"]
                    competitors_count: 3
                    realtime: false
                    content_markdown: "For a startup, the strongest picks right now are:\n\n- **Acme CRM** for teams under 20 [1]\n- HubSpot when you need marketing tools in the same place [2]"
                meta:
                  project: "64f1a2b3c4d5e6f708192a3b"
                  timezone: "America/New_York"
                  from: "2026-08-01"
                  to: "2026-08-31"
                  models: ["chatgpt", "gemini", "perplexity"]
                  prompt_id: 2048
                  brand_mentioned: null
                  include: ["content"]
                  limit: 100
                  count: 93
                  has_more: false
                  next_cursor: null
                credits: { charged: 2, remaining: 2945 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7t8"
        "400":
          $ref: "#/components/responses/BadWindow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/responses/{response}:
    get:
      tags: [Responses]
      operationId: getResponse
      summary: Get response
      description: |
        One answer in full: the row from List responses, the whole answer as Markdown, every page the model cited or consulted, every competitor it named, every domain it recommended with its position, and any shopping tiles it showed.

        This is the prompt page detail for one answer, in one call.

        Cost: 2 API credits.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - name: response
          in: path
          required: true
          description: The response id. From List responses.
          schema: { type: integer }
      responses:
        "200":
          description: The answer with everything found in it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ResponseDetail"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      timezone: { type: string }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: 918273
                  prompt: { id: 2048, text: "best crm for a startup" }
                  model: "chatgpt"
                  date: "2026-08-31"
                  collected_at: "2026-08-31T03:12:40+00:00"
                  brand_mentioned: true
                  position: "top"
                  sentiment: "positive"
                  search_performed: true
                  search_queries: ["best crm startups 2026"]
                  competitors_count: 2
                  realtime: false
                  content_markdown: "For a startup, the strongest picks right now are:\n\n- **Acme CRM** for teams under 20 [1]\n- HubSpot when you need marketing tools in the same place [2]"
                  sources:
                    - { url: "https://www.acme.com/pricing", domain: "acme.com", cited: true, cite_rank: 1 }
                    - { url: "https://www.hubspot.com/products/crm", domain: "hubspot.com", cited: true, cite_rank: 2 }
                    - { url: "https://www.g2.com/categories/crm", domain: "g2.com", cited: false, cite_rank: null }
                  competitors:
                    - { domain: "hubspot.com", url: "https://www.hubspot.com/products/crm" }
                    - { domain: "pipedrive.com", url: "https://www.pipedrive.com" }
                  rankings:
                    - { domain: "acme.com", url: "https://www.acme.com", position: 1 }
                    - { domain: "hubspot.com", url: "https://www.hubspot.com", position: 2 }
                  products: []
                meta: { project: "64f1a2b3c4d5e6f708192a3b", timezone: "America/New_York" }
                credits: { charged: 2, remaining: 2943 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7t9"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: "`PROJECT_NOT_FOUND`: no project with that id. `NOT_FOUND`: no response with that id in this project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/sources:
    get:
      tags: [Sources]
      operationId: listSources
      summary: List sources
      description: |
        One row per web page an answer cited or consulted. `cited: true` means the page was linked in the answer and `cite_rank` is its number. `cited: false` means the model read it but did not link it.

        Use it: to see the exact pages behind a domain, for example after Top source domains.

        Cost: 1 API credit per page.

        Limits: Up to 31 days in one call. Up to 100 rows per page.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/prompt_id"
        - name: domain
          in: query
          description: Only pages on this domain, exactly as stored.
          schema: { type: string, maxLength: 255 }
        - name: cited
          in: query
          description: "`true` for pages linked in the answer, `false` for pages only consulted."
          schema: { type: boolean }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: One page of sources.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Source"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: 5512001
                    prompt: { id: 2048, text: "best crm for a startup" }
                    model: "chatgpt"
                    date: "2026-08-31"
                    collected_at: "2026-08-31T03:12:40+00:00"
                    url: "https://www.g2.com/categories/crm"
                    domain: "g2.com"
                    cited: true
                    cite_rank: 1
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt"], prompt_id: null, domain: "g2.com", cited: true, limit: 50, count: 1, has_more: false, next_cursor: null }
                credits: { charged: 1, remaining: 2942 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u0"
        "400":
          $ref: "#/components/responses/BadWindow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/sources/top-domains:
    get:
      tags: [Sources]
      operationId: topSourceDomains
      summary: Top source domains
      description: |
        The domains AI answers relied on most, ranked by how often they appeared. Each domain shows its share of all source mentions, how many times it was linked, how many prompts it appeared for, which models used it and when it was last seen.

        `type` tells you whether the domain is your `brand`, a `competitor` you configured, or `other`.

        Send `cited=true` to rank by inline citations only: the pages an answer linked right next to a sentence, not the ones it only consulted. Shares and counts are then out of inline citations.

        Use it: to find the sites worth pitching or publishing on.

        Cost: 2 API credits. Results are cached for 10 minutes.

        Limits: Up to 31 days in one call. Up to 100 domains. 10 calls per minute per key.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - name: cited
          in: query
          description: "`true` counts only pages linked in the answer (inline citations), `false` only pages consulted. Default: all."
          schema: { type: boolean }
        - name: limit
          in: query
          description: Domains to return, 1 to 100. Default 20.
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        "200":
          description: The domains, most used first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TopDomain"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                      models: { type: array, items: { type: string } }
                      cited: { type: [boolean, "null"], description: "The cited filter used; null when not sent." }
                      limit: { type: integer }
                      count: { type: integer }
                      total_domains: { type: integer, description: "Distinct domains in the range." }
                      total_mentions: { type: integer, description: "Source rows in the range." }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - { domain: "g2.com", type: "other", mentions: 214, share: 11.7, cited: 180, prompts: 31, models: ["chatgpt", "gemini"], last_seen: "2026-08-31" }
                  - { domain: "acme.com", type: "brand", mentions: 172, share: 9.4, cited: 160, prompts: 40, models: ["chatgpt", "gemini", "perplexity"], last_seen: "2026-08-31" }
                  - { domain: "hubspot.com", type: "competitor", mentions: 151, share: 8.3, cited: 120, prompts: 28, models: ["chatgpt", "gemini"], last_seen: "2026-08-30" }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt", "gemini", "perplexity"], cited: null, limit: 20, count: 3, total_domains: 325, total_mentions: 1824 }
                credits: { charged: 2, remaining: 2940 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u1"
        "400":
          description: "`DATE_RANGE_TOO_LARGE`: more than 31 days."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/competitors:
    get:
      tags: [Competitors]
      operationId: getCompetitors
      summary: Get share of voice
      description: |
        Who the answers recommend, how often, and at what position. Domains are grouped by their registrable name, so `www.hubspot.com` and `blog.hubspot.com` count as one.

        `brand` is your own entry with its rank. `competitors` is everyone else, most recommended first, up to 100. `is_configured_competitor` marks the ones you set up in the project.

        Cost: 2 API credits. Results are cached for 10 minutes.

        Limits: Up to 31 days in one call. 10 calls per minute per key.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
      responses:
        "200":
          description: Share of voice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      total_mentions: { type: integer }
                      brand:
                        oneOf:
                          - $ref: "#/components/schemas/ShareOfVoiceEntry"
                          - type: "null"
                        description: Your brand's entry, or null when it was never recommended in the range.
                      competitors:
                        type: array
                        items:
                          $ref: "#/components/schemas/ShareOfVoiceEntry"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                      models: { type: array, items: { type: string } }
                      count: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  total_mentions: 1240
                  brand: { domain: "acme.com", mentions: 117, share: 9.4, average_position: 2.1, is_brand: true, is_configured_competitor: false, rank: 1 }
                  competitors:
                    - { domain: "hubspot.com", mentions: 98, share: 7.9, average_position: 2.6, is_brand: false, is_configured_competitor: true, rank: 2 }
                    - { domain: "pipedrive.com", mentions: 74, share: 6.0, average_position: 3.4, is_brand: false, is_configured_competitor: true, rank: 3 }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt", "gemini", "perplexity"], count: 2 }
                credits: { charged: 2, remaining: 2938 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u2"
        "400":
          description: "`DATE_RANGE_TOO_LARGE`: more than 31 days."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/rankings:
    get:
      tags: [Rankings]
      operationId: listRankings
      summary: List rankings
      description: |
        One row per domain an answer recommended, with its position in that answer. Position 1 is the first recommendation.

        Use it: with `domain` set to your own domain to see exactly where you ranked in each answer.

        Cost: 1 API credit per page.

        Limits: Up to 31 days in one call. Up to 100 rows per page.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/prompt_id"
        - name: domain
          in: query
          description: Only this domain, exactly as stored.
          schema: { type: string, maxLength: 255 }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: One page of rankings.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Ranking"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: 7712001
                    prompt: { id: 2048, text: "best crm for a startup" }
                    model: "chatgpt"
                    date: "2026-08-31"
                    collected_at: "2026-08-31T03:12:40+00:00"
                    domain: "acme.com"
                    url: "https://www.acme.com"
                    position: 1
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt"], prompt_id: null, domain: "acme.com", limit: 50, count: 1, has_more: false, next_cursor: null }
                credits: { charged: 1, remaining: 2937 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u3"
        "400":
          $ref: "#/components/responses/BadWindow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/query-fanout:
    get:
      tags: [Query fanout]
      operationId: listQueryFanout
      summary: List query fanout
      description: |
        The web searches a model ran behind the scenes before answering. One row per answer that searched, with the list of queries.

        Use it: to see the exact phrases the model looks for, and write for them.

        Cost: 1 API credit per page.

        Limits: Up to 31 days in one call. Up to 100 rows per page.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/prompt_id"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: One page of answers with their searches.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/QueryFanoutRow"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - response_id: 918273
                    prompt: { id: 2048, text: "best crm for a startup" }
                    model: "chatgpt"
                    date: "2026-08-31"
                    collected_at: "2026-08-31T03:12:40+00:00"
                    queries: ["best crm startups 2026", "acme crm pricing", "hubspot free crm limits"]
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt", "gemini", "perplexity"], prompt_id: 2048, limit: 50, count: 1, has_more: false, next_cursor: null }
                credits: { charged: 1, remaining: 2936 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u4"
        "400":
          $ref: "#/components/responses/BadWindow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/web-search:
    get:
      tags: [Web search]
      operationId: getWebSearch
      summary: Get web search
      description: |
        How often the models ran a web search before answering, and what they searched for. The share of answers that searched, then one entry per prompt with the distinct search queries, how many times each was seen, and the average number of queries per search.

        Use it: pass `prompt_id` for one prompt (what the Web Search card on its page shows), or leave it out for every active prompt, most queries first (the Query Fanout table).

        Cost: 2 API credits.

        Limits: Up to 31 days in one call. `limit` caps the prompts returned (default 50, up to 100); `meta.prompts_total` says how many there were.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/prompt_id"
        - $ref: "#/components/parameters/limit"
      responses:
        "200":
          description: The search rate and the queries per prompt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/WebSearch"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                      models: { type: array, items: { type: string } }
                      prompt_id: { type: [integer, "null"] }
                      limit: { type: integer }
                      count: { type: integer }
                      prompts_total: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  answers: 18
                  searched: 16
                  search_rate: 88.9
                  prompts:
                    - prompt: { id: 2048, text: "best crm for a startup" }
                      answers: 18
                      searched: 16
                      search_rate: 88.9
                      query_count: 10
                      avg_queries_per_search: 1.8
                      queries:
                        - { query: "best crm startups 2026", count: 3 }
                        - { query: "hubspot free crm limits", count: 1 }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt", "gemini", "perplexity"], prompt_id: 2048, limit: 50, count: 1, prompts_total: 1 }
                credits: { charged: 2, remaining: 2934 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u5"
        "400":
          $ref: "#/components/responses/BadWindow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/sentiment:
    get:
      tags: [Sentiment]
      operationId: getSentiment
      summary: Get sentiment
      description: |
        How the answers that mention your brand talk about it: how many were positive, mixed and negative, as counts and as shares, overall and per model.

        Cost: 2 API credits. Results are cached for 10 minutes.

        Limits: Up to 31 days in one call. 10 calls per minute per key.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
        - $ref: "#/components/parameters/model"
      responses:
        "200":
          description: Sentiment counts and shares.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      total: { type: integer, description: "Answers that mentioned the brand and have a sentiment." }
                      counts:
                        $ref: "#/components/schemas/SentimentCounts"
                      share:
                        $ref: "#/components/schemas/SentimentShares"
                      by_model:
                        type: object
                        additionalProperties:
                          type: object
                          properties:
                            total: { type: integer }
                            counts:
                              $ref: "#/components/schemas/SentimentCounts"
                            share:
                              $ref: "#/components/schemas/SentimentShares"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                      models: { type: array, items: { type: string } }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  total: 48
                  counts: { positive: 31, mixed: 12, negative: 5 }
                  share: { positive: 64.6, mixed: 25.0, negative: 10.4 }
                  by_model:
                    chatgpt: { total: 30, counts: { positive: 21, mixed: 7, negative: 2 }, share: { positive: 70.0, mixed: 23.3, negative: 6.7 } }
                    gemini: { total: 18, counts: { positive: 10, mixed: 5, negative: 3 }, share: { positive: 55.6, mixed: 27.8, negative: 16.7 } }
                meta: { project: "64f1a2b3c4d5e6f708192a3b", from: "2026-08-01", to: "2026-08-31", models: ["chatgpt", "gemini"] }
                credits: { charged: 2, remaining: 2934 }
                request_id: "req_01j9x4v9l0m1n2o3p4q5r6s7u5"
        "400":
          description: "`DATE_RANGE_TOO_LARGE`: more than 31 days."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"
        "504":
          $ref: "#/components/responses/QueryTimeout"

  /projects/{project}/exports/estimate:
    post:
      tags: [Exports]
      operationId: estimateExport
      summary: Estimate an export
      description: |
        Tells you exactly how many rows an export would have and whether you have enough Export rows for it. Same body as Create export. Nothing is created and nothing is charged.

        Use it: before every export. The count is exact, so what you see here is what Create export will charge.

        Cost: Free.

        Limits: 10 calls per minute per key.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
      requestBody:
        $ref: "#/components/requestBodies/ExportPlan"
      responses:
        "200":
          description: The normalised plan and the estimate.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      plan:
                        $ref: "#/components/schemas/ExportPlan"
                      estimate:
                        type: object
                        properties:
                          rows: { type: integer, description: "Exact row count." }
                          rows_available: { type: integer, description: "Export rows left on the owner's account." }
                          rows_limit: { type: integer, description: "The plan's monthly Export rows." }
                          enough: { type: boolean, description: "True when rows is above zero and within rows_available." }
                          max_rows: { type: integer, description: "Rows one file may hold." }
                          too_large: { type: boolean, description: "True when rows is above max_rows." }
                  meta:
                    type: object
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  plan: { type: "responses", format: "csv", from: "2026-06-01", to: "2026-08-31", models: ["chatgpt", "gemini"], prompt_ids: [2048, 2049, 2050], topic_ids: [17], include_answer: true }
                  estimate: { rows: 2340, rows_available: 30000, rows_limit: 30000, enough: true, max_rows: 250000, too_large: false }
                meta: {}
                credits: { charged: 0, remaining: null }
                request_id: "req_01j9x4w5u6v7w8x9y0z1a2b3c4"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}/exports:
    post:
      tags: [Exports]
      operationId: createExport
      summary: Create export
      description: |
        Queues an export. Charges the project owner's Export rows for the exact row count, then writes the file in the background. Keep the `id` and poll Get export until `status` is `ready`.

        Answers in a `responses` export come as Markdown, the same text the prompt page shows. CSV files have a header row and a UTF-8 byte order mark. NDJSON files have one JSON object per line.

        Cost: Export rows, one per row. No API credits. Rows are refunded if the export fails.

        Limits: Up to 365 days. Up to 250,000 rows per file. One export at a time per person, three per project owner. 10 calls per minute per key.

        Needs: Write. Your seat must be allowed to manage content.
      parameters:
        - $ref: "#/components/parameters/project"
      requestBody:
        $ref: "#/components/requestBodies/ExportPlan"
      responses:
        "202":
          description: The export is queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Export"
                  meta:
                    type: object
                    properties:
                      rows_charged: { type: integer }
                      export_rows_remaining: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: "9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b"
                  project: "64f1a2b3c4d5e6f708192a3b"
                  type: "responses"
                  format: "csv"
                  status: "queued"
                  from: "2026-06-01"
                  to: "2026-08-31"
                  models: ["chatgpt", "gemini"]
                  prompt_ids: [2048, 2049, 2050]
                  topic_ids: [17]
                  include_answer: true
                  estimated_rows: 2340
                  rows: 0
                  bytes: null
                  rows_charged: 2340
                  file_name: "acme-crm-responses-2026-06-01-to-2026-08-31.csv.gz"
                  download_url: null
                  error: null
                  requested_by: "Ada Lovelace"
                  created_at: "2026-09-03T10:15:02+00:00"
                  started_at: null
                  finished_at: null
                  expires_at: null
                meta: { rows_charged: 2340, export_rows_remaining: 27660 }
                credits: { charged: 0, remaining: 2945 }
                request_id: "req_01j9x4x1d2e3f4g5h6i7j8k9l0"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`INSUFFICIENT_EXPORT_ROWS`: not enough Export rows for this many rows. `PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Write permission. `PROJECT_FORBIDDEN`: the key cannot reach this project. `PERMISSION_DENIED`: your seat cannot export."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "409":
          description: "`EXPORT_IN_PROGRESS`: you already have an export running, or the owner has three."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: "`VALIDATION_FAILED`: a field is wrong, `details` says which. `EXPORT_EMPTY`: no rows match. `EXPORT_TOO_LARGE`: more than 250,000 rows, split by date or topic."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      tags: [Exports]
      operationId: listExports
      summary: List exports
      description: |
        The last 50 exports for the project, newest first, with status and a download link for the ready ones.

        Cost: 1 API credit.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/project"
      responses:
        "200":
          description: The exports.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Export"
                  meta:
                    type: object
                    properties:
                      count: { type: integer }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  - id: "9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b"
                    project: "64f1a2b3c4d5e6f708192a3b"
                    type: "responses"
                    format: "csv"
                    status: "ready"
                    from: "2026-06-01"
                    to: "2026-08-31"
                    models: ["chatgpt", "gemini"]
                    prompt_ids: [2048, 2049, 2050]
                    topic_ids: [17]
                    include_answer: true
                    estimated_rows: 2340
                    rows: 2340
                    bytes: 1382211
                    rows_charged: 2340
                    file_name: "acme-crm-responses-2026-06-01-to-2026-08-31.csv.gz"
                    download_url: "https://api.radarkit.ai/v1/exports/9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b/download"
                    error: null
                    requested_by: "Ada Lovelace"
                    created_at: "2026-09-03T10:15:02+00:00"
                    started_at: "2026-09-03T10:15:04+00:00"
                    finished_at: "2026-09-03T10:15:41+00:00"
                    expires_at: "2026-09-10T10:15:41+00:00"
                meta: { count: 1 }
                credits: { charged: 1, remaining: 2944 }
                request_id: "req_01j9x4x1d2e3f4g5h6i7j8k9l1"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ProjectNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /exports/{export}:
    get:
      tags: [Exports]
      operationId: getExport
      summary: Get export
      description: |
        The status of one export. Poll this every few seconds after Create export.

        `queued` is waiting for a worker. `running` is being written and `rows` counts up. `ready` is done and `download_url` is set. `failed` says what went wrong in `error` and the rows were refunded. `expired` means the file was deleted after 7 days.

        Cost: Free.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/export"
      responses:
        "200":
          description: The export.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Export"
                  meta:
                    type: object
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: "9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b"
                  project: "64f1a2b3c4d5e6f708192a3b"
                  type: "responses"
                  format: "csv"
                  status: "ready"
                  from: "2026-06-01"
                  to: "2026-08-31"
                  models: ["chatgpt", "gemini"]
                  prompt_ids: [2048, 2049, 2050]
                  topic_ids: [17]
                  include_answer: true
                  estimated_rows: 2340
                  rows: 2340
                  bytes: 1382211
                  rows_charged: 2340
                  file_name: "acme-crm-responses-2026-06-01-to-2026-08-31.csv.gz"
                  download_url: "https://api.radarkit.ai/v1/exports/9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b/download"
                  error: null
                  requested_by: "Ada Lovelace"
                  created_at: "2026-09-03T10:15:02+00:00"
                  started_at: "2026-09-03T10:15:04+00:00"
                  finished_at: "2026-09-03T10:15:41+00:00"
                  expires_at: "2026-09-10T10:15:41+00:00"
                meta: {}
                credits: { charged: 0, remaining: null }
                request_id: "req_01j9x4x1d2e3f4g5h6i7j8k9l2"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Read permission. `PROJECT_FORBIDDEN`: the key cannot reach the export's project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`NOT_FOUND`: no export with that id that this key can see."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /exports/{export}/download:
    get:
      tags: [Exports]
      operationId: downloadExport
      summary: Download export
      description: |
        Sends you to the file. The answer is a `302` to a signed link that works for 30 minutes, so tell your HTTP client to follow redirects. With curl, pass `-L`. The file is gzip.

        Cost: Free.

        Needs: Read.
      parameters:
        - $ref: "#/components/parameters/export"
      responses:
        "302":
          description: Follow the `Location` header to the file.
          headers:
            Location:
              description: A signed link to the gzip file, valid for 30 minutes.
              schema: { type: string, format: uri }
        "200":
          description: The gzip file streamed directly, when a signed link is not available.
          content:
            application/gzip:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Read permission. `PROJECT_FORBIDDEN`: the key cannot reach the export's project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`NOT_FOUND`: no export with that id that this key can see."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: "`EXPORT_NOT_READY`: the export is still queued or running, or it failed or expired. `details.status` says which."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "EXPORT_NOT_READY"
                  message: "This export is running."
                  details: { status: "running" }
                request_id: "req_01j9x4x1d2e3f4g5h6i7j8k9l3"
        "429":
          $ref: "#/components/responses/RateLimited"

  /projects/{project}/content/articles:
    post:
      tags: [Content]
      operationId: createArticle
      summary: Create article
      description: |
        Has the Content agent write an article for one of your prompts. The agent reads the pages AI models cite for that prompt, builds an outline and writes in your brand's voice. The draft lands in your editor. Writing takes a few minutes, so keep the run `id` and poll Get article run.

        If an article for the same prompt, type and keyword is already being written or is done, you get that run back with `meta.reused: true` and no content credit is spent. Send `force: true` to write another.

        Articles started here appear in the project's Agents section on a sheet named API.

        Cost: 1 API credit plus 1 AI content credit from the project owner. Nothing is charged when the request is refused.

        Limits: 10 calls per minute per key.

        Needs: Agents. Your seat must be allowed to manage content.
      parameters:
        - $ref: "#/components/parameters/project"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prompt_id, keyword]
              properties:
                prompt_id:
                  type: integer
                  description: The prompt the article targets.
                article_type:
                  type: string
                  enum: [blog_post, listicle, comparison, alternatives, faq, first_person_review]
                  description: The article format. Default is the format the cited pages use most.
                keyword:
                  type: string
                  maxLength: 255
                  description: The keyword the article should rank for, as in the Create Content dialog.
                country:
                  type: string
                  minLength: 2
                  maxLength: 2
                  description: Two-letter country the article should rank in; drives the SERP research. Default is the project region.
                language:
                  type: string
                  enum: [en, da, de, es, fr, it, pt, nl, pl, ru]
                  description: Language of the SERP research and of the article. Default is English.
                additional_details:
                  type: string
                  maxLength: 1500
                  description: Instructions for the writer.
                force:
                  type: boolean
                  description: Write a new article even if one for the same prompt, type and keyword exists.
                sheet:
                  type: string
                  maxLength: 80
                  description: Name of the content sheet the article row goes on, under Agents → Sheets. Default is the project's "API" sheet. A sheet holds 50 rows; a full one rolls over to "Name 2" and `meta.sheet` tells you where the row landed.
            example:
              prompt_id: 2048
              article_type: "comparison"
              keyword: "best crm for startups"
              country: "US"
              language: "en"
              additional_details: "Lead with pricing. Mention the free tier for teams under 3."
      responses:
        "202":
          description: The run has started.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContentRun"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                      reused: { type: boolean, description: "True when an existing run was returned instead of starting a new one." }
                      ai_content_credits_remaining: { type: integer }
                      next_step: { type: string }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: 551
                  project: "64f1a2b3c4d5e6f708192a3b"
                  status: "processing"
                  current_step: "creating"
                  error: null
                  retryable: false
                  prompt: { id: 2048 }
                  article:
                    id: 812
                    title: null
                    keyword: "best crm for startups"
                    article_type: "comparison"
                    write_mode: "ai"
                    status: "draft"
                    crawl_status: "pending"
                    outline_status: "pending"
                    word_count: null
                    scores: { radarkit: null, seo: null, geo: null, readability: null }
                    editor_url: "https://radarkit.ai/dashboard/64f1a2b3c4d5e6f708192a3b/content/812"
                    created_at: "2026-09-03T10:30:00+00:00"
                    updated_at: "2026-09-03T10:30:00+00:00"
                  started_at: "2026-09-03T10:30:00+00:00"
                  completed_at: null
                meta:
                  project: "64f1a2b3c4d5e6f708192a3b"
                  reused: false
                  ai_content_credits_remaining: 19
                  next_step: "Poll GET /v1/content/runs/{id} until status is completed, then open editor_url or fetch the body with include=body."
                credits: { charged: 1, remaining: 2941 }
                request_id: "req_01j9x519n0o1p2q3r4s5t6u7v8"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`INSUFFICIENT_CONTENT_CREDITS`: the owner has no AI content credits. `INSUFFICIENT_CREDITS`: no API credits left. `PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Agents permission. `PROJECT_FORBIDDEN`: the key cannot reach this project. `PERMISSION_DENIED`: your seat cannot manage content."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`PROJECT_NOT_FOUND`: no project with that id. `PROMPT_NOT_FOUND`: no active prompt with that id in this project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: "`VALIDATION_FAILED`: a field is wrong, `details` says which. `CONTENT_REJECTED`: the Content agent refused the request, the message says why."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /content/runs/{run}:
    get:
      tags: [Content]
      operationId: getArticleRun
      summary: Get article run
      description: |
        The status of one article run. Poll this every few seconds after Create article.

        `processing` is working and `current_step` says where it is. `completed` is done and the article has a title, a word count and scores. `failed` and `cancelled` say what happened and can be retried.

        Add `include=body` once the run is completed to get the article as Markdown in `body_markdown`.

        Cost: Free. 2 API credits with `include=body`.

        Needs: Agents.
      parameters:
        - $ref: "#/components/parameters/run"
        - name: include
          in: query
          description: "`body` adds the article as Markdown. Costs 2 credits."
          schema: { type: string, enum: [body] }
      responses:
        "200":
          description: The run.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContentRun"
                  meta:
                    type: object
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: 551
                  project: "64f1a2b3c4d5e6f708192a3b"
                  status: "completed"
                  current_step: "done"
                  error: null
                  retryable: false
                  prompt: { id: 2048 }
                  article:
                    id: 812
                    title: "The best CRM for startups in 2026: Acme vs HubSpot vs Pipedrive"
                    keyword: "best crm for startups"
                    article_type: "comparison"
                    write_mode: "ai"
                    status: "draft"
                    crawl_status: "completed"
                    outline_status: "completed"
                    word_count: 1840
                    scores: { radarkit: 82.0, seo: 78.5, geo: 85.0, readability: 71.0 }
                    editor_url: "https://radarkit.ai/dashboard/64f1a2b3c4d5e6f708192a3b/content/812"
                    created_at: "2026-09-03T10:30:00+00:00"
                    updated_at: "2026-09-03T10:36:12+00:00"
                  started_at: "2026-09-03T10:30:00+00:00"
                  completed_at: "2026-09-03T10:36:12+00:00"
                meta: {}
                credits: { charged: 0, remaining: null }
                request_id: "req_01j9x519n0o1p2q3r4s5t6u7v9"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          description: "`INSUFFICIENT_CREDITS`: no API credits left for `include=body`. `PLAN_INACTIVE` or `OWNER_PLAN_INACTIVE`: a plan is not active."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: "`SCOPE_MISSING`: the key has no Agents permission. `PROJECT_FORBIDDEN`: the key cannot reach the run's project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`RUN_NOT_FOUND`: no content run with that id that this key can see."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          $ref: "#/components/responses/ValidationFailed"
        "429":
          $ref: "#/components/responses/RateLimited"

  /content/runs/{run}/retry:
    post:
      tags: [Content]
      operationId: retryArticleRun
      summary: Retry article run
      description: |
        Restarts a failed or cancelled run. It keeps its article and spends no new content credit. A run that is still processing or already completed is refused.

        Cost: 1 API credit.

        Limits: 20 calls per minute per key.

        Needs: Agents.
      parameters:
        - $ref: "#/components/parameters/run"
      responses:
        "202":
          description: The run is processing again.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContentRun"
                  meta:
                    type: object
                    properties:
                      project: { type: string }
                  credits:
                    $ref: "#/components/schemas/Credits"
                  request_id:
                    $ref: "#/components/schemas/RequestId"
              example:
                data:
                  id: 551
                  project: "64f1a2b3c4d5e6f708192a3b"
                  status: "processing"
                  current_step: "creating"
                  error: null
                  retryable: false
                  prompt: { id: 2048 }
                  article:
                    id: 812
                    title: null
                    keyword: "best crm for startups"
                    article_type: "comparison"
                    write_mode: "ai"
                    status: "draft"
                    crawl_status: "pending"
                    outline_status: "pending"
                    word_count: null
                    scores: { radarkit: null, seo: null, geo: null, readability: null }
                    editor_url: "https://radarkit.ai/dashboard/64f1a2b3c4d5e6f708192a3b/content/812"
                    created_at: "2026-09-03T10:30:00+00:00"
                    updated_at: "2026-09-03T10:41:00+00:00"
                  started_at: "2026-09-03T10:41:00+00:00"
                  completed_at: null
                meta: { project: "64f1a2b3c4d5e6f708192a3b" }
                credits: { charged: 1, remaining: 2940 }
                request_id: "req_01j9x519n0o1p2q3r4s5t6u7w0"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: "`SCOPE_MISSING`: the key has no Agents permission. `PROJECT_FORBIDDEN`: the key cannot reach the run's project."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "`RUN_NOT_FOUND`: no content run with that id that this key can see."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: "`RUN_NOT_RETRYABLE`: the run is processing or completed. Only failed and cancelled runs can be retried."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Your API key. Create one in Settings, API keys. Send it as `Authorization: Bearer rk_live_YOUR_KEY`."

  parameters:
    project:
      name: project
      in: path
      required: true
      description: The project id. 24 characters. From List projects.
      schema:
        type: string
        pattern: "^[0-9a-f]{24}$"
      example: "64f1a2b3c4d5e6f708192a3b"
    export:
      name: export
      in: path
      required: true
      description: The export id. From Create export or List exports.
      schema:
        type: string
        format: uuid
      example: "9b2f6a3e-1c4d-4e5f-8a9b-0c1d2e3f4a5b"
    run:
      name: run
      in: path
      required: true
      description: The run id. From Create article.
      schema:
        type: integer
      example: 551
    from:
      name: from
      in: query
      description: "First day, `YYYY-MM-DD`, in the project's timezone. Default: the window your dashboard shows, 7 or 30 days back."
      schema: { type: string, format: date }
      example: "2026-08-01"
    to:
      name: to
      in: query
      description: "Last day, `YYYY-MM-DD`, included. Default: today."
      schema: { type: string, format: date }
      example: "2026-08-31"
    model:
      name: model
      in: query
      description: "One model name, or several separated by commas: `chatgpt`, `gemini`, `perplexity`, `copilot`, `aimode`, `aioverview`. Default: every model the project tracks. Names the plan does not include are ignored."
      schema: { type: string, maxLength: 120 }
      example: "chatgpt,gemini"
    prompt_id:
      name: prompt_id
      in: query
      description: Only rows for this prompt.
      schema: { type: integer, minimum: 1 }
    limit:
      name: limit
      in: query
      description: Rows per page, 1 to 100. Default 50.
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    cursor:
      name: cursor
      in: query
      description: "`next_cursor` from the previous page. Keep the other filters the same."
      schema: { type: string, maxLength: 200 }

  requestBodies:
    ExportPlan:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [type]
            properties:
              type:
                type: string
                enum: [responses, sources, competitors, rankings, query_fanout]
                description: "What to export. `responses` is one row per AI answer with brand mention, position, sentiment, competitors, sources cited and the answer as Markdown. `sources` is one row per page cited or consulted. `competitors` is one row per competitor named. `rankings` is one row per recommended domain with its position. `query_fanout` is one row per search query the model ran."
              format:
                type: string
                enum: [csv, ndjson]
                default: csv
                description: "`csv` with a header row, or `ndjson` with one JSON object per line."
              from:
                type: string
                format: date
                description: "First day, `YYYY-MM-DD`. Default: 30 days ago."
              to:
                type: string
                format: date
                description: "Last day, `YYYY-MM-DD`, included. Default: today."
              models:
                type: array
                maxItems: 10
                items: { type: string }
                description: "Models to include. Default: every model the project tracks."
              topic_ids:
                type: array
                maxItems: 100
                items: { type: integer }
                description: Every active prompt in these topics.
              prompt_ids:
                type: array
                maxItems: 500
                items: { type: integer }
                description: Only these prompts. Leave both this and topic_ids out to export every prompt.
              include_answer:
                type: boolean
                default: true
                description: "`responses` only. Set false to leave the answer text out and get a much smaller file."
          example:
            type: "responses"
            format: "csv"
            from: "2026-06-01"
            to: "2026-08-31"
            models: ["chatgpt", "gemini"]
            topic_ids: [17]
            include_answer: true

  responses:
    Unauthenticated:
      description: "`UNAUTHENTICATED`: no key, a wrong key, or a revoked or expired key. `KEY_IN_QUERY` (400) if the key was in the URL."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "UNAUTHENTICATED"
              message: "Missing or invalid API key. Send it as: Authorization: Bearer rk_live_YOUR_KEY"
            request_id: "req_01j9x4q7z8y9x0w1v2u3t4s5r6"
    PaymentRequired:
      description: "`INSUFFICIENT_CREDITS`: no API credits left, nothing ran. `PLAN_INACTIVE`: the key owner's plan is not active. `OWNER_PLAN_INACTIVE`: the project owner's plan is not active."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "INSUFFICIENT_CREDITS"
              message: "Not enough API credits."
            request_id: "req_01j9x4q7z8y9x0w1v2u3t4s5r7"
    ScopeMissing:
      description: "`SCOPE_MISSING`: the key lacks the permission this call needs. `API_FROZEN`: API access for the account is paused. `PLAN_NO_API`: the plan has no API access."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "SCOPE_MISSING"
              message: "This key does not have the \"read\" permission."
              details: { required_scope: "read" }
            request_id: "req_01j9x4q7z8y9x0w1v2u3t4s5r8"
    Forbidden:
      description: "`SCOPE_MISSING`: the key lacks the permission this call needs. `PROJECT_FORBIDDEN`: the key cannot reach this project. `API_FROZEN`: API access for the account is paused."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "PROJECT_FORBIDDEN"
              message: "This key cannot access that project."
            request_id: "req_01j9x4q7z8y9x0w1v2u3t4s5r9"
    ProjectNotFound:
      description: "`PROJECT_NOT_FOUND`: no project with that id."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "PROJECT_NOT_FOUND"
              message: "No project with that id."
            request_id: "req_01j9x4q7z8y9x0w1v2u3t4s5s0"
    ValidationFailed:
      description: "`VALIDATION_FAILED`: a parameter is missing or wrong. `details` lists each field."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "VALIDATION_FAILED"
              message: "One or more parameters are invalid."
              details:
                - { path: "from", message: "The from field must match the format Y-m-d." }
            request_id: "req_01j9x4s6b7c8d9e0f1g2h3i4j5"
    BadWindow:
      description: "`DATE_RANGE_TOO_LARGE`: more than 31 days, use an export. `INVALID_CURSOR`: the cursor does not match these filters, start from the first page."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "DATE_RANGE_TOO_LARGE"
              message: "The date range can span at most 31 days. Use exports for more."
              details: { max_days: 31 }
            request_id: "req_01j9x4s6b7c8d9e0f1g2h3i4j6"
    BadCursor:
      description: "`INVALID_CURSOR`: the cursor does not match these filters. Start from the first page."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "INVALID_CURSOR"
              message: "The cursor is not valid for this request."
            request_id: "req_01j9x4s6b7c8d9e0f1g2h3i4j7"
    RateLimited:
      description: "`RATE_LIMITED`: too many calls. Wait the seconds in `Retry-After` and send the same request again. Nothing was charged."
      headers:
        Retry-After:
          description: Seconds to wait.
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "RATE_LIMITED"
              message: "Too many requests. Slow down and retry."
              details: { retry_after: 21 }
            request_id: "req_01j9x4r2s3t4u5v6w7x8y9z0a1"
    QueryTimeout:
      description: "`QUERY_TIMEOUT`: the query took too long. Nothing was charged. Narrow the range or create an export."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "QUERY_TIMEOUT"
              message: "The query took too long. Narrow the date range or use exports."
            request_id: "req_01j9x4r2s3t4u5v6w7x8y9z0a2"

  schemas:
    RequestId:
      type: string
      description: A unique id for this call. Also in the `X-Request-Id` header. Quote it when you contact support.
      example: "req_01j9x4m2p7q8r9s0t1u2v3w4x5"

    Credits:
      type: object
      description: What this call cost and what is left. Also in the `X-RadarKit-Credits-Charged` and `X-RadarKit-Credits-Remaining` headers.
      properties:
        charged:
          type: integer
          description: API credits spent on this call.
        remaining:
          type: [integer, "null"]
          description: API credits left on the billed account. Null on free calls.

    Error:
      type: object
      description: Every failed call has this shape and no `data`.
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable. Build on this, not on the message.
            message:
              type: string
              description: For people. May change.
            details:
              description: More when there is more to say. Which field failed, how long to wait, which id already exists.
              oneOf:
                - type: object
                - type: array
                  items: { type: object }
        request_id:
          $ref: "#/components/schemas/RequestId"

    PageMeta:
      type: object
      description: Facts about the request and how to get the next page.
      properties:
        project: { type: string }
        from: { type: string, format: date }
        to: { type: string, format: date }
        models: { type: array, items: { type: string } }
        limit: { type: integer }
        count: { type: integer, description: "Rows on this page." }
        has_more: { type: boolean }
        next_cursor:
          type: [string, "null"]
          description: Send back as `cursor` to get the next page.

    Account:
      type: object
      properties:
        user:
          type: object
          properties:
            id: { type: integer }
            name: { type: string }
            email: { type: string }
        key:
          type: object
          properties:
            name: { type: string }
            prefix: { type: string, description: "The first characters of the key, for telling keys apart." }
            scopes: { type: array, items: { type: string, enum: [read, write, agents] } }
            restricted_to_projects: { type: boolean }
            expires_at: { type: [string, "null"], format: date-time }
        plan:
          type: object
          properties:
            name: { type: string }
            display_name: { type: string }
            status: { type: string }
            renews_at: { type: [string, "null"], format: date-time }
        credits:
          $ref: "#/components/schemas/Balance"
        export_rows:
          $ref: "#/components/schemas/Balance"
        rate_limits:
          type: object
          properties:
            perMinute: { type: integer }
            perSecond: { type: integer }

    Balance:
      type: object
      properties:
        available: { type: integer, description: "Plan plus top-up." }
        plan: { type: integer, description: "Left in the plan bucket. Refills on renewal." }
        additional: { type: integer, description: "Top-up. Does not expire." }
        plan_limit: { type: integer, description: "The plan's monthly allowance." }

    Project:
      type: object
      properties:
        id: { type: string, description: "24 characters. Never changes." }
        name: { type: string }
        url: { type: string }
        region: { type: string }
        timezone: { type: string, description: "Day boundaries follow this." }
        models: { type: array, items: { type: string }, description: The models the owner's plan tracks. }
        prompts_count: { type: integer }
        next_run_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }

    Topic:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        status: { type: string }
        prompts_count: { type: integer, description: "Active prompts in the topic. Only on List topics." }
        created_at: { type: [string, "null"], format: date-time }

    Prompt:
      type: object
      properties:
        id: { type: integer }
        text: { type: string }
        topic_id: { type: [integer, "null"] }
        last_checked_at: { type: [string, "null"], format: date-time }
        created_at: { type: [string, "null"], format: date-time }
        models:
          type: object
          description: "Status per model: `pending`, `processing`, `completed` or `hold`. Only on List prompts."
          additionalProperties: { type: [string, "null"] }

    Response:
      type: object
      properties:
        id: { type: integer }
        prompt:
          type: object
          properties:
            id: { type: integer }
            text: { type: [string, "null"] }
        model: { type: string }
        date: { type: string, format: date, description: "The day the answer was collected, as the dashboard groups it." }
        collected_at: { type: string, format: date-time, description: "The exact moment, in UTC." }
        brand_mentioned: { type: [boolean, "null"] }
        position: { type: [string, "null"], enum: [top, middle, bottom, null], description: "Where the brand appeared." }
        sentiment: { type: [string, "null"], enum: [positive, mixed, negative, null] }
        search_performed: { type: [boolean, "null"], description: "True when the model searched the web before answering." }
        search_queries: { type: array, items: { type: string } }
        competitors_count: { type: integer }
        realtime: { type: [boolean, "null"], description: "True when the answer came from Run a prompt now, not the schedule." }
        content_markdown: { type: string, description: "The answer as Markdown. Only with `include=content`." }

    ResponseDetail:
      allOf:
        - $ref: "#/components/schemas/Response"
        - type: object
          properties:
            content_markdown: { type: string, description: "The whole answer as Markdown with a numbered source list." }
            sources:
              type: array
              items:
                type: object
                properties:
                  url: { type: string }
                  domain: { type: [string, "null"] }
                  cited: { type: [boolean, "null"] }
                  cite_rank: { type: [integer, "null"] }
            competitors:
              type: array
              items:
                type: object
                properties:
                  domain: { type: [string, "null"] }
                  url: { type: string }
            rankings:
              type: array
              items:
                type: object
                properties:
                  domain: { type: [string, "null"] }
                  url: { type: string }
                  position: { type: [integer, "null"] }
            products:
              type: array
              description: Shopping tiles, when the answer showed any.
              items:
                type: object
                properties:
                  name: { type: string }
                  merchant: { type: [string, "null"] }
                  merchant_domain: { type: [string, "null"] }
                  url: { type: [string, "null"] }
                  price: { type: [string, "null"] }
                  group: { type: integer }
                  position: { type: integer }

    Source:
      type: object
      properties:
        id: { type: integer }
        prompt:
          type: object
          properties:
            id: { type: integer }
            text: { type: [string, "null"] }
        model: { type: string }
        date: { type: string, format: date }
        collected_at: { type: string, format: date-time }
        url: { type: string }
        domain: { type: [string, "null"] }
        cited: { type: [boolean, "null"], description: "True when the page was linked in the answer." }
        cite_rank: { type: [integer, "null"], description: "The citation number in the answer." }

    TopDomain:
      type: object
      properties:
        domain: { type: string }
        type: { type: string, enum: [brand, competitor, other] }
        mentions: { type: integer, description: "Source rows for the domain." }
        share: { type: number, description: "Percent of all source rows in the range." }
        cited: { type: integer, description: "Rows where the page was linked." }
        prompts: { type: integer, description: "Prompts the domain appeared for." }
        models: { type: array, items: { type: string } }
        last_seen: { type: string, format: date }

    ShareOfVoiceEntry:
      type: object
      properties:
        domain: { type: string }
        mentions: { type: integer }
        share: { type: number, description: "Percent of all recommendations in the range." }
        average_position: { type: [number, "null"], description: "Mention-weighted average position in the answers." }
        is_brand: { type: boolean }
        is_configured_competitor: { type: boolean }
        rank: { type: integer }

    Ranking:
      type: object
      properties:
        id: { type: integer }
        prompt:
          type: object
          properties:
            id: { type: integer }
            text: { type: [string, "null"] }
        model: { type: string }
        date: { type: string, format: date }
        collected_at: { type: string, format: date-time }
        domain: { type: [string, "null"] }
        url: { type: string }
        position: { type: [integer, "null"], description: "1 is the first recommendation." }

    QueryFanoutRow:
      type: object
      properties:
        response_id: { type: integer }
        prompt:
          type: object
          properties:
            id: { type: integer }
            text: { type: [string, "null"] }
        model: { type: string }
        date: { type: string, format: date }
        collected_at: { type: string, format: date-time }
        queries: { type: array, items: { type: string } }

    WebSearch:
      type: object
      properties:
        answers: { type: integer, description: "Answers in the window." }
        searched: { type: integer, description: "Answers that ran a web search." }
        search_rate: { type: number, description: "searched as a share of answers, 0 to 100." }
        prompts:
          type: array
          items:
            type: object
            properties:
              prompt:
                type: object
                properties:
                  id: { type: integer }
                  text: { type: [string, "null"] }
              answers: { type: integer }
              searched: { type: integer }
              search_rate: { type: number }
              query_count: { type: integer, description: "Distinct search queries." }
              avg_queries_per_search: { type: number, description: "Average queries per answer that searched." }
              queries:
                type: array
                items:
                  type: object
                  properties:
                    query: { type: string }
                    count: { type: integer, description: "Answers that ran this query." }

    VisibilityAverage:
      type: object
      properties:
        visibility: { type: number }
        brand_score: { type: number }
        days: { type: integer }

    VisibilityDay:
      type: object
      properties:
        date: { type: string, format: date }
        model: { type: string }
        visibility: { type: number }
        brand_score: { type: number }
        positions:
          type: object
          properties:
            top: { type: integer }
            middle: { type: integer }
            bottom: { type: integer }
        sentiment:
          $ref: "#/components/schemas/SentimentCounts"

    SentimentCounts:
      type: object
      properties:
        positive: { type: integer }
        mixed: { type: integer }
        negative: { type: integer }

    SentimentShares:
      type: object
      description: Percent of the total.
      properties:
        positive: { type: number }
        mixed: { type: number }
        negative: { type: number }

    ExportPlan:
      type: object
      description: The request after defaults were filled in.
      properties:
        type: { type: string, enum: [responses, sources, competitors, rankings, query_fanout] }
        format: { type: string, enum: [csv, ndjson] }
        from: { type: string, format: date }
        to: { type: string, format: date }
        models: { type: array, items: { type: string } }
        prompt_ids: { type: array, items: { type: integer }, description: The prompts that will be exported. Filled in from topic_ids when you gave topics. }
        topic_ids: { type: array, items: { type: integer } }
        include_answer: { type: boolean }

    Export:
      type: object
      properties:
        id: { type: string, format: uuid }
        project: { type: string }
        type: { type: string, enum: [responses, sources, competitors, rankings, query_fanout] }
        format: { type: string, enum: [csv, ndjson] }
        status: { type: string, enum: [queued, running, ready, failed, expired] }
        from: { type: string, format: date }
        to: { type: string, format: date }
        models: { type: array, items: { type: string } }
        prompt_ids: { type: array, items: { type: integer } }
        topic_ids: { type: array, items: { type: integer } }
        include_answer: { type: boolean }
        estimated_rows: { type: integer }
        rows: { type: integer, description: "Rows written so far. Counts up while running." }
        bytes: { type: [integer, "null"], description: "File size once ready." }
        rows_charged: { type: integer }
        file_name: { type: string }
        download_url: { type: [string, "null"], description: "Set when the status is ready." }
        error: { type: [string, "null"] }
        requested_by: { type: [string, "null"] }
        created_at: { type: [string, "null"], format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
        expires_at: { type: [string, "null"], format: date-time, description: "When the file is deleted. 7 days after it was ready." }

    ContentRun:
      type: object
      properties:
        id: { type: integer }
        project: { type: string }
        status: { type: string, enum: [processing, completed, failed, cancelled] }
        current_step: { type: [string, "null"] }
        error: { type: [string, "null"] }
        retryable: { type: boolean, description: "True for failed and cancelled runs." }
        prompt:
          type: object
          properties:
            id: { type: [integer, "null"] }
        article:
          type: [object, "null"]
          properties:
            id: { type: integer }
            title: { type: [string, "null"] }
            keyword: { type: [string, "null"] }
            article_type: { type: [string, "null"] }
            write_mode: { type: [string, "null"] }
            status: { type: [string, "null"] }
            crawl_status: { type: [string, "null"] }
            outline_status: { type: [string, "null"] }
            word_count: { type: [integer, "null"] }
            scores:
              type: object
              properties:
                radarkit: { type: [number, "null"] }
                seo: { type: [number, "null"] }
                geo: { type: [number, "null"] }
                readability: { type: [number, "null"] }
            editor_url: { type: [string, "null"], description: "Open this in the dashboard to edit and publish." }
            created_at: { type: [string, "null"], format: date-time }
            updated_at: { type: [string, "null"], format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
        body_markdown: { type: string, description: "The article as Markdown. Only with `include=body`." }
