Citations
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.
What a citation carries
Section titled “What a citation carries”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.
Cite by ref, not by page
Section titled “Cite by ref, not by 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.
Preview a citation
Section titled “Preview 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.
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.
The rendered page
Section titled “The rendered page”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.
curl -s "$KOVAC_BASE/api/manage/citations/$CHUNK_ID/preview/image?mode=crop" \ -H "Authorization: Bearer $KOVAC_API_KEY" \ -o citation.pngmode 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.
When has_image is false
Section titled “When has_image is false”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.
Boxes are fractions, not pixels
Section titled “Boxes are fractions, not pixels”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
Section titled “Confidence”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 asupportedcitation 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.
From an embedded widget
Section titled “From an embedded widget”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:
# 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.
How precisely a citation highlights
Section titled “How precisely a citation highlights”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.
