Skip to content

Citations

This content is not available in your language yet.

A citation is only useful if a reader can check it. This page covers what a citation carries in the search tool’s result, and the two endpoints that turn one into a page you can actually look at.

Each row the search tool returns is a passage plus everything you need to address it:

{
"ref": "9f2a1b7c",
"chunk_id": "9f2a1b7c-3e51-4b8a-9c2d-771f2a6e5d10",
"document_id": "3d0e7c44-9a11-4a2e-8b6f-2c9a5d1e0f33",
"content": "Expenses over 100 EUR require prior approval...",
"source_uri": "upload://handbook.pdf",
"collection": "handbook",
"metadata": { "page": 4, "section": "Expenses" },
"confidence": "supported"
}

chunk_id and document_id are what the preview endpoints below take. page and section stay inside metadata, exactly as before — they are already there, and duplicating them at the top level would just be a second place for the two copies to disagree.

On a collection chunked hierarchically a citation may cover a whole section: content is then the section’s text and metadata carries page_end beside page. chunk_id is still the passage that matched, so the preview endpoints below land on that passage, highlighted on its own page.

The model is asked to cite a result by writing its ref value in square brackets — [9f2a1b7c] — never the page number, and never source_uri.

The reason is what a page number actually identifies. Measured against a real corpus: 99.6% of page-bearing chunks share their page with at least one sibling — a table of contents entry and the paragraph below it, two clauses on the same page, a heading and its body. A citation naming only a page tells a reader which page to look at, but not which of several chunks on it the answer actually came from. ref names the exact chunk. It is a short prefix of the chunk’s id — long enough to be unique within one result set, extended automatically if two ever collide — so it survives being written into an answer, read back later, and resolved against the preview endpoints without a second round trip through search.

Never render source_uri directly in an answer either. It is what you render a citation link from — the document’s real home if you set one at upload, otherwise an internal placeholder — but showing the raw value to a reader is a path leak, not a citation.

GET /api/manage/citations/{chunk_id}/preview returns everything a citation panel needs except the image: where the passage came from, its text, and the boxes to outline if there is a page to draw them on.

Terminal window
curl -s "$KOVAC_BASE/api/manage/citations/$CHUNK_ID/preview" \
-H "Authorization: Bearer $KOVAC_API_KEY"
{
"chunk_id": "9f2a1b7c-3e51-4b8a-9c2d-771f2a6e5d10",
"document_id": "3d0e7c44-9a11-4a2e-8b6f-2c9a5d1e0f33",
"collection": "handbook",
"collection_id": "7b1a9e20-...",
"source_uri": "upload://handbook.pdf",
"page": 4,
"section": "Expenses",
"snippet": "Expenses over 100 EUR require prior approval from your line manager...",
"boxes": [{ "page": 4, "x0": 0.12, "y0": 0.31, "x1": 0.88, "y1": 0.37 }],
"has_image": true
}

snippet is a bounded excerpt of the chunk’s text (up to 400 characters), not the whole chunk — enough for a hover card, not a way to read a document a passage at a time through this endpoint. collection_id is included alongside the human-readable collection name specifically so a UI can link into the collection without a second call just to look up its id.

There is no confidence field on this response. Confidence belongs to a retrieval — this endpoint takes no query — so if you want to show it next to a preview, carry it over from the search result you already have rather than expecting the preview to know it.

GET /api/manage/citations/{chunk_id}/preview/image returns the cited page as a PNG, with the chunk’s boxes drawn on it — or, with ?mode=crop, just that region.

Terminal window
curl -s "$KOVAC_BASE/api/manage/citations/$CHUNK_ID/preview/image?mode=crop" \
-H "Authorization: Bearer $KOVAC_API_KEY" \
-o citation.png

mode is either page (the default, full page) or crop. Anything else is a 422, not a silent fall back — asking for a crop and getting a full page would look like a bug rather than a typo.

Only PDF pages can be rendered. Every other format — Markdown, HTML, spreadsheets, plain text, all of it — reports has_image: false, and the right thing to do with that is show the snippet instead. If your corpus is not PDF-heavy, design the citation panel around the snippet fallback from the start rather than treating it as an edge case.

has_image is also false for a PDF chunk the parser could not place on the page — no bounding box, so nothing to outline. That is content-level, not a fault: some PDFs do not parse down to page geometry, and the snippet is the honest answer for those chunks too.

A 404 from the image endpoint is the same outcome as has_image: false on the preview response, not an error to surface — the document simply has no page image for this citation. Treat it as a signal to fall back, the same as a false you saw in advance.

Every box in boxes — on the preview response and implicitly behind the rendered image — is expressed as x0/y0/x1/y1 fractions of the page, 0 to 1, top-left origin. The server does not know what size you will render the page at, so it cannot hand you pixels; multiply by your own rendered width and height to draw the outline.

confidence on a search result is one of two values:

  • supported — the passage’s rerank score met or exceeded the bar your collection uses to decide whether to accept a result or broaden the query and retry. It is the same bar the retrieval pipeline itself judges by, so a supported citation is never one the pipeline treated internally as too weak to keep.
  • weak — the score fell short of that bar. The passage was still the best available match; treat it as a citation worth a second look rather than one to hide.

That bar is your collection’s own score_threshold, shown in the UI as Confidence bar (retry & citations) and editable on the collection’s Settings tab. It is also readable and settable on GET/PATCH /collections/{collection_id} — see Collections & knowledge — and covered as a retrieval-quality knob in Improving retrieval quality. If you have not set one, the platform’s own default applies; set an explicit value on the collection if you want supported to mean the same thing regardless of any future change to that default.

confidence is null when the reranker was unavailable for that retrieval — not when it scored a passage low. A null means “not scored,” and a client must not treat it the same as "weak". Rendering nothing (no badge at all) is the correct response to null; rendering a “weak” badge would report a measurement that was never taken.

An embedded widget’s visitor holds a publishable kve_ key, never a tenant API key, and the citation endpoints do not accept one directly.

A kve_-scoped variant of the preview endpoint does not exist, and should not: a kve_ key is accepted at exactly two endpoints on the whole platform, POST /embed/v1/session and GET /embed/v1/config, and that short list is deliberate. The flow is instead a two-step exchange:

Terminal window
# 1. Exchange the publishable key for a short-lived visitor token.
curl -s -X POST "$KOVAC_BASE/embed/v1/session" \
-H "Content-Type: application/json" \
-d '{"key": "kve_your_publishable_key"}'
# => {"token": "<visitor bearer token>", "expires_in": 900, "visitor_handle": "..."}
# 2. Use that token, not the kve_ key, on the citation routes.
curl -s "$KOVAC_BASE/api/manage/citations/$CHUNK_ID/preview" \
-H "Authorization: Bearer $VISITOR_TOKEN"

If you hold a kve_ key and send it as a Bearer token on either citation route directly, you will get a 401 — not a bug, just the wrong credential for this surface. Mint a session first.

A visitor token only unlocks chunks that visitor’s own searches actually returned — not every chunk in the tenant, and not another visitor’s citations. Each grant expires (24 hours by default, tunable per deployment); after it expires, the preview endpoint answers 404 for that chunk, indistinguishably from a chunk that never existed. A returning visitor who asks the widget the same question again gets a fresh grant the same way as the first time — there is nothing to renew by hand.

When a visitor taps a citation in an embedded assistant, the widget can jump to the source page and highlight the passage on it. You choose how precisely it highlights:

  • Best match (the default) highlights the part of the passage that scored best for the question, when there is one, and otherwise the whole passage. It costs nothing extra.
  • Whole passage highlights the entire passage the answer drew on. It costs nothing extra.
  • Model’s quote highlights the exact words a small model picked as the support for each citation. Each quote is checked against the passage and is only ever the passage’s own text; one that does not match is dropped and the highlight falls back to the best match. It adds one small model call to each answer that has citations, a little after the answer appears.

Set it under Citation highlight in the agent editor’s Knowledge section, or in a workflow’s Settings dialog. The Embed tab’s On the page section can override it for one embed; leave it on “Same as the agent” (or “Same as the workflow”) to follow it. If the words cannot be found on the page, the widget tries the whole passage, and then, when previews are on, opens the passage in its preview panel instead.