> ## Documentation Index
> Fetch the complete documentation index at: https://docs.querying.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Citation Analysis

> Per-prompt AI visibility diagnosis: the searches an engine ran, the pages it retrieved and cited, the brands its answer names and the pages behind them, where your brand stands, and prioritized actions

Compare a completed AI answer with the pages behind it. The result shows the
searches the engine ran, the pages it retrieved and the ones it cited, the
brands the answer names and the pages that name them, where your brand stands,
and prioritized actions. It also maps the paragraphs of each cited page to the
parts of the answer they correspond to, with an explanation for every link, and
sums up what the cited pages have in common.

This is post-hoc correspondence analysis of an existing answer — not proof that
a source caused the model to generate a sentence, and not a score of a page's
quality. Treat it as evidence to inspect, not a verdict.

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/citation-analysis \
  -H "Authorization: Bearer $QUERYING_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "answer": "…the AI answer, as raw markdown…",
    "prompt": "best password manager for a small team",
    "engine": "CHATGPT",
    "brand": "1Password",
    "competitors": ["Bitwarden"],
    "citations": [
      { "url": "https://example.com/best-password-managers", "body": "…the page markdown…" },
      { "url": "https://another.example.com/pricing" }
    ]
  }'
```

Poll `GET /v1/async/task/:id`, or supply `webhook.url` when submitting; the task
reports `taskType: "CITATION_ANALYSIS"`. Do not submit this analysis through
`POST /v1/async/task`; use the dedicated endpoint. Earlier names,
including GEO Audit's, keep working: `POST /v1/geo-audit`,
`POST /v1/source-influence` and `POST /v1/research`, the `GEO_AUDIT`,
`SOURCE_INFLUENCE` and `CITATION_ATTRIBUTION` task types, and
`include.geoAudit` and `include.sourceInfluence`;
responses always use the new names.

### Analyze a task you already ran

If the answer came from this API, send its task id instead of copying the
answer and citations out of the result:

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/citation-analysis \
  -H "Authorization: Bearer $QUERYING_API_KEY" -H "Content-Type: application/json" \
  -d '{ "taskId": "7c1f…", "brand": "1Password", "competitors": ["Bitwarden"] }'
```

The task must be one of yours, `COMPLETED`, and still readable through
`GET /v1/async/task/:id`. Its answer Markdown (text when the engine returned no
Markdown) becomes `answer`, and its cited URLs become `citations` in the
engine's order: the first 100, with at most 10 discussion threads. Product and
video card placeholders (`googleusercontent.com` links) and Google search links
are skipped, since they have no page to read. The task's
`prompt` and `country` and its engine are used unless you send them. Sending
`taskId` together with `answer` or `citations` is rejected with `400`, and so
is a task that is unfinished or has no answer text or cited URL. An unknown or
expired task id returns `404`.

### Analyze every answer as you collect it

Add `include.citationAnalysis: true` to a collection request, and the analysis
runs on the answer as soon as it is collected. It works with the answer engines
(`CHATGPT`, `GEMINI`, `PERPLEXITY`, `GOOGLE_AIO`, `GOOGLE_AIMODE`, `NAVER_AI_BRIEF`, `NAVER_AI_TAB`, `BING_SEARCH`, `BING_COPILOT`), in single tasks, task batches, grids and `POST /v1/batches`.

```bash theme={"theme":"css-variables"}
curl -X POST https://api.querying.ai/v1/async/task \
  -H "Authorization: Bearer $QUERYING_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "taskType": "CHATGPT",
    "payload": { "prompt": "best password manager for a small team", "country": "US", "include": { "citationAnalysis": true } },
    "webhook": { "url": "https://example.com/hook" }
  }'
```

The analysis reads the answer and its cited URLs the same way `taskId` does
above. The answer's result then carries `response.citationAnalysis`: the
analysis task exactly as `GET /v1/async/task/:id` returns it, with `task`,
`credits` and `response` or `error`.

* **Webhook**: sent once, after the analysis finishes, with the answer and the analysis together.
* **Polling**: the answer task is `COMPLETED` as soon as the answer is ready. Keep polling until `response.citationAnalysis.task.status` is `COMPLETED` or `FAILED`.
* **Batches** complete when every answer and its analysis have finished.
* **No analysis**: when the answer cites no URL to read, or your credits or the key's daily limit cannot cover the analysis hold, `response.citationAnalysis` is `{ "error": "…" }` and the answer is delivered right away.
* **Billing**: the analysis is charged on its own, by usage like any Citation Analysis task. The answer's charge does not change. A key limited to certain task types must also allow `CITATION_ANALYSIS`.

## Payload

| Field | Required | Notes |
| - | - | - |
| `taskId` | instead of `answer` + `citations` | A completed answer task of yours; see above. |
| `answer` | yes, unless `taskId` | Original answer Markdown, 1–200,000 characters. Keep it unchanged; every range is a UTF-16 offset into exactly this text. |
| `citations[]` | yes, unless `taskId` | The pages the engine cited, and optionally pages it retrieved but did not cite: at most 100 cited and 50 non-cited, at least one cited, at most 10 discussion-thread URLs. |
| `citations[].url` | yes | HTTP(S) URL, at most 2,048 characters. |
| `citations[].body` | no | Source Markdown, at most 300,000 characters per citation; the whole request body is at most 1 MiB. A supplied body is analyzed as-is, never replaced by a current fetch. Without a body we fetch the page; a page that could not be read is reported on that source with `error`. |
| `citations[].kind` | no | `inline` or `panel`; detected from answer links when omitted. |
| `citations[].cited` | no | Defaults to `true`. `false` marks a page the engine retrieved (a search result) but did not cite. |
| `citations[].position` | no | Integer ≥ 1: the engine's own rank for the page in its search results. |
| `citations[].query` | no | The search (≤ 500 characters) that found the page; added to `queries` if missing. |
| `citations[].citedText[]` | no | Up to 20 passages (≤ 2,000 characters each) the engine itself reported citing. |
| `citations[].publishedAt` | no | Publication date as the engine reported it, ≤ 64 characters. |
| `queries[]` | no | Up to 20 searches the engine ran, ≤ 500 characters each. |
| `prompt` | no | Original question. |
| `engine` | no | Which AI engine produced the answer. |
| `brand` | no | Your brand name. With it, the result adds `visibility` and `actions`. |
| `brandDomains[]` | no | Up to 10 hostnames of your brand's own sites, such as `coway.com`: bare hostnames of letters, digits, dots and hyphens, at most 253 characters, without scheme or path. Requires `brand`; sent without it, the request is rejected with `422`. Without `brandDomains`, a host whose name contains the brand name counts as the brand's own site. |
| `brandAliases[]` | no | Up to 10 other names of your brand (product lines, local spellings), each ≤ 200 characters. Requires `brand`; sent without it, the request is rejected with `422`. |
| `competitors[]` | no | Up to 25 competitor names, reported in `brands` with role `competitor`. |
| `country` | no | Source-fetch country, defaults to `US`. |

With `taskId` or `include.citationAnalysis`, these fields are filled from the
answer's raw data. For `CHATGPT`, the engine's searches become `queries` and every
page it retrieved becomes a citation with `cited`, `position`, `citedText`,
`publishedAt` and `query`; the same URL with different tracking parameters is
merged. For Google AI Overview, the overview's sources are cited and organic
results outside them are non-cited, with `position` set to their organic rank.
Other engines provide cited sources only.

> [!WARNING]
>
> The request rejects `analysis` with `400`. Insights are included in every result.

## Result

Results stored before v4 keep their earlier shape, without a `version` field.

```json theme={"theme":"css-variables"}
{
  "version": 4,
  "answer": "For most homes, the Coway Airmega 250 is the best all-round pick. Levoit Core 300 is a strong budget choice, and Blueair Blue Pure 211+ suits large rooms.",
  "queries": [
    { "id": "q0", "text": "best air purifiers 2026 reviews", "sourceIds": ["s0", "s1", "s2"] }
  ],
  "sources": [
    {
      "id": "s0",
      "url": "https://www.example-reviews.com/best-air-purifiers",
      "domain": "example-reviews.com",
      "channel": "editorial",
      "cited": true,
      "position": 1,
      "queryIds": ["q0"],
      "publishedAt": "October 7, 2026",
      "citedText": ["Levoit Core 300 is the best budget purifier we tested."],
      "brandIds": ["b1", "b2"],
      "own": false,
      "paragraphs": [
        { "id": "s0p0", "text": "Levoit Core 300 is the best budget purifier we tested." },
        { "id": "s0p1", "text": "For large rooms, the Blueair Blue Pure 211+ cleared the air fastest." }
      ]
    },
    {
      "id": "s1",
      "url": "https://www.coway.com/airmega-250",
      "domain": "coway.com",
      "channel": "own",
      "cited": false,
      "position": 4,
      "queryIds": ["q0"],
      "publishedAt": null,
      "citedText": [],
      "brandIds": ["b0"],
      "own": true,
      "paragraphs": []
    },
    {
      "id": "s2",
      "url": "https://www.reddit.com/r/AirPurifiers/comments/abc123/budget_pick",
      "domain": "reddit.com",
      "channel": "community",
      "cited": true,
      "position": 6,
      "queryIds": ["q0"],
      "publishedAt": null,
      "citedText": [],
      "brandIds": ["b1"],
      "own": false,
      "paragraphs": [
        { "id": "s2p0", "text": "Two years with the Levoit Core 300 and it is still quiet on sleep mode." }
      ]
    }
  ],
  "brands": [
    { "id": "b0", "name": "Coway", "role": "target", "rank": 1, "answerRanges": [{ "start": 20, "end": 25 }], "citedSourceIds": [], "retrievedSourceIds": ["s1"], "ownSourceIds": ["s1"] },
    { "id": "b1", "name": "Levoit", "role": "competitor", "rank": 2, "answerRanges": [{ "start": 66, "end": 72 }], "citedSourceIds": ["s0", "s2"], "retrievedSourceIds": [], "ownSourceIds": [] },
    { "id": "b2", "name": "Blueair", "role": "answer", "rank": 3, "answerRanges": [{ "start": 113, "end": 120 }], "citedSourceIds": ["s0"], "retrievedSourceIds": [], "ownSourceIds": [] }
  ],
  "links": [
    { "id": "l0", "paragraphId": "s0p0", "answerRanges": [{ "start": 66, "end": 154 }], "explanation": "Names the Levoit Core 300 as the budget pick." },
    { "id": "l1", "paragraphId": "s0p1", "answerRanges": [{ "start": 66, "end": 154 }], "explanation": "Recommends the Blueair Blue Pure 211+ for large rooms." },
    { "id": "l2", "paragraphId": "s2p0", "answerRanges": [{ "start": 66, "end": 154 }], "explanation": "Reports long-term use of the Levoit Core 300." }
  ],
  "visibility": { "brandId": "b0", "stage": "mentioned", "rank": 1, "answerBrands": 3 },
  "actions": [
    {
      "id": "a0",
      "type": "improve_page",
      "control": "owned",
      "priority": 1,
      "summary": "Coway's Airmega 250 page was retrieved for this search but not cited.",
      "steps": [
        "Add a table of room coverage, noise level and yearly filter cost, the criteria the cited review compares.",
        "Show when the specifications were last updated."
      ],
      "sourceIds": ["s1"],
      "brandIds": ["b0"],
      "queryIds": ["q0"],
      "linkIds": []
    },
    {
      "id": "a1",
      "type": "publish_page",
      "control": "owned",
      "priority": 2,
      "summary": "No Coway page was cited for this search; the cited review is a dated buying guide.",
      "steps": [
        "Publish a 2026 air purifier buying guide on coway.com, organized by room size and budget.",
        "Explain how models were compared, show measured results, and name the author and date."
      ],
      "sourceIds": ["s0"],
      "brandIds": ["b0"],
      "queryIds": ["q0"],
      "linkIds": ["l0", "l1"]
    },
    {
      "id": "a2",
      "type": "post_on_platform",
      "control": "platform",
      "priority": 3,
      "summary": "A cited Reddit thread recommends Levoit and never mentions Coway.",
      "steps": [
        "Reply as an identified Coway representative with the Airmega 250 details the thread asks about."
      ],
      "sourceIds": ["s2"],
      "brandIds": ["b1"],
      "queryIds": ["q0"],
      "linkIds": ["l2"]
    },
    {
      "id": "a3",
      "type": "pitch_publisher",
      "control": "outreach",
      "priority": 4,
      "summary": "The top cited review names Levoit and Blueair but not Coway; only its publisher can add the Airmega 250.",
      "steps": [
        "Offer the editors an Airmega 250 for testing.",
        "Send test data for the criteria the review uses: room coverage, noise and filter cost."
      ],
      "sourceIds": ["s0"],
      "brandIds": ["b1", "b2"],
      "queryIds": ["q0"],
      "linkIds": ["l0", "l1"]
    }
  ],
  "insights": [
    { "summary": "Both cited pages recommend Levoit for a budget; the review also backs Blueair for large rooms.", "sourceIds": ["s0", "s2"], "linkIds": ["l0", "l1", "l2"] },
    { "summary": "The answer names Coway first, yet no cited page names it; only its own page, which was not cited, does.", "sourceIds": ["s1"], "linkIds": [] }
  ]
}
```

### `version`

Always `4` for this shape.

### `answer`

The answer you submitted, unchanged. `answerRanges` are offsets into this exact
string: JavaScript UTF-16 code units, `start` inclusive, `end` exclusive. Slice
it as-is — `answer.slice(start, end)` — instead of re-splitting the text.
Each `links[]` range identifies a whole semantic unit of your answer: a sentence,
list item or table cell. Each `brands[]` range is the exact name as written.
Boundaries refer to the original text.

### `queries[]`

The searches the engine ran for this answer. Each has an `id`, its `text`, and
`sourceIds`: the sources that search found. Empty when the engine exposed no
searches and you sent none.

### `sources[]` — one entry per page

* `id` — opaque identifier, unique within the result.
* `url` — the page URL; `domain` — its host.
* `channel` — who controls the page, judged from its host:
  * `own` — your brand's own site (exactly when `own` is `true`).
  * `brand` — the own site of another brand the result names.
  * `editorial` — a publisher; only the publisher can change the page. Hosts
    that are not recognized are reported as `editorial`.
  * `retailer` — an online store or marketplace.
  * `review_platform` — a customer or business review site.
  * `community` — a forum or Q\&A site.
  * `video` — a video platform.
  * `social` — a social network.
  * `blog_platform` — a hosted blog service.
  * `reference` — an encyclopedia or wiki; never the target of an action.
* `cited` — `true` when the answer cites the page, `false` when the engine only
  retrieved it.
* `position` — the engine's own rank for the page, or `null` when it gave none.
* `queryIds[]` — the searches that found the page.
* `publishedAt` — the publication date as the engine reported it, or `null`.
* `citedText[]` — passages the engine itself reported citing; `[]` when none.
* `brandIds[]` — brands named on the readable page, its title or `citedText`.
* `own` — `true` when the page is on your brand's own site.
* `paragraphs[]` — the paragraphs of a cited page included in the result. Each
  has an `id` (globally unique across the whole result) and the `text` of the
  paragraph. A paragraph can be included with no link at all. Always empty for
  non-cited pages.
* `error` — present only when the page could not be read. `paragraphs` is then
  empty, and the string is a short reason for that source.

### `brands[]`

Every brand the result tracks: your `brand` (`role: "target"`), your
`competitors` (`"competitor"`), and other brands the answer names (`"answer"`).

* `rank` — the order of the brand's first name span in the answer, starting at
  1; `null` when the answer does not name the brand.
* `answerRanges[]` — the exact spans where the answer names the brand or one of
  its product names, in the same UTF-16 offsets, so `answer.slice(start, end)`
  returns the name as written. Matching ignores case and takes whole words
  (except in scripts written without spaces); it covers headings and table
  cells, never link URLs or image markup. Unlike `links[]` ranges, these are not
  whole answer units.
* `citedSourceIds[]` — cited sources whose page names it.
* `retrievedSourceIds[]` — non-cited sources whose page names it.
* `ownSourceIds[]` — sources on the brand's own site; filled for the target only.

### `links[]` — one relationship per paragraph

* `paragraphId` — the paragraph this link is about. It always points at a
  paragraph that exists in `sources[]`.
* `answerRanges[]` — the parts of the answer this paragraph corresponds to, as
  `start`/`end` pairs in the same UTF-16 offsets as above. A link may carry
  several ranges; the same answer text is not repeated across them. Each range
  is a whole answer unit (sentence, list item or table cell).
* `explanation` — a short sentence describing the correspondence: what the
  paragraph states about that part of the answer.

### `visibility`

Where your brand stands in this answer; `null` when no `brand` was sent.

* `brandId` — your brand in `brands[]`.
* `stage` — one of:
  * `mentioned` — the answer names your brand.
  * `on_cited_page` — the answer does not name it, but a cited page does.
  * `on_retrieved_page` — only pages the engine retrieved without citing name it.
  * `not_found` — no page that was read names it.
* `rank` — your brand's `rank` in `brands[]`.
* `answerBrands` — how many brands the answer names.

### `actions[]`

Prioritized next steps for your brand; `[]` when no `brand` was sent. `priority`
starts at 1 (do first) and has no gaps. Actions are ordered by `control`: what
your brand controls first (`owned`), then platforms it can post on
(`platform`), then outreach (`outreach`). `summary` is one sentence and
`steps[]` holds one to five concrete steps, both in the answer's language.
`sourceIds`, `brandIds`, `queryIds` and `linkIds` point at the evidence in this
result.

| `type` | `control` | When it appears | `sourceIds` |
| - | - | - | - |
| `improve_page` | `owned` | One per site of yours: your own pages there were retrieved but not cited, or cited while the answer does not name your brand. Steps say what to add or change so the pages answer the prompt the way the cited pages do: criteria, numbers, freshness. | Up to three of your pages on that site, not-cited pages first. |
| `publish_page` | `owned` | For a search where none of your pages was cited: a brief for a new page on your own site, modelled on the cited pages — page type and title, sections and criteria, evidence to show, date, clear brand authorship. | The cited pages to model. |
| `post_on_platform` | `platform` | A page on a retailer, review platform, community, video, social or blog platform names rival brands but not yours. The action is your brand's own listing, profile, answer or post on that platform, from your own account: listing or improving your product, claiming your profile and asking real customers for reviews, answering as an identified brand representative, or publishing a demo or comparison. It never asks the platform or the page's owner for anything. At most one per domain. | The cited page on that platform, which belongs to someone else. |
| `pitch_publisher` | `outreach` | A cited editorial page names rival brands but not yours, or names yours while the answer does not. Only the publisher can change it; steps say what to send. | The page to pitch. |

`brandIds` names your brand, or the rival brands those pages name. Actions
never suggest fake or undisclosed reviews, posing as an independent reviewer,
buying links, or editing pages your brand does not own. Third-party pages that
name your brand but were not cited are listed in `brands[].retrievedSourceIds`,
not as actions.

### `insights[]`

Up to four short summaries of what the pages add up to: which channels were
cited, the format, freshness or criteria the cited pages share, how the answer's
brand order compares with what the cited pages cover, and where your brand sits.
`sourceIds` names the pages and `linkIds` the links each summary rests on; at
least one of them is non-empty, and they only name entries in this result.
Insights describe correspondence; they are not advice.

## Reading the result correctly

* **No link is not a verdict.** A paragraph without a link simply did not match an
  answer range, and an answer part with no range means no paragraph was linked to
  it. Neither says the page is irrelevant, unused or untrustworthy.
* **An unread page stays uncertain.** When `sources[].error` is set, that citation
  was excluded from the analysis; do not treat its lack of links as a negative
  finding. Retry it or supply its `body` if it matters.
* **Correspondence, not causation.** The result describes how an existing answer
  and existing page text line up after the fact, and which brands those pages
  name. It does not measure generation influence, score the page, or report how
  the answer was produced.
* **`not_found` means not on any page read.** It does not mean your brand is absent
  from the web.
* **Retrieval data depends on the engine.** Non-cited pages appear only when the
  engine exposes them (ChatGPT, Google AI Overview) or you send `cited: false`
  pages. Otherwise every source is cited, and your own page can only appear as
  cited.
* **Channels are judged from the host.** `channel` is inferred from the page's
  host, not from reading who wrote it.
* **Actions are suggestions, not guarantees.** They are prioritized from the
  pages listed in this result; acting on one does not ensure the answer changes.
* **Ids are internal.** Do not persist `s…`/`q…`/`b…`/`l…`/`a…` identifiers as
  stable keys across runs; they are scoped to a single result.

## Execution and limits

* **Size is not a failure mode.** Anything the request contract accepts — an
  `answer` up to 200,000 characters with up to 100 citations — is analyzed. Long
  answers, large source sets and very long pages are handled by splitting the
  work, not by rejecting the task.
* **Depth scales with the answer, not the page.** A long page does not produce
  more links than the answer can support: the analysis concentrates on the
  passages most likely to correspond to the answer, and every source that shows
  a correspondence is represented. On a large page, or on a page that repeats
  the same statement in navigation, listings and reviews, expect the strongest
  passages rather than every occurrence.
* **Markup is not evidence.** Sitemaps, link indexes and image galleries
  contribute no links; a page made only of those is reported with no paragraphs
  rather than with invented correspondence.
* A source that could not be read is reported in `sources[].error` without
  failing the task if another source is readable. The task fails when no source
  could be read, on an analysis failure, on a timeout, or on a service
  configuration error.
* Requests are metered by usage, like the rest of the API. The submit response
  shows the temporary credit hold; the final charge is settled when the task
  reaches a terminal state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.