PlanCover Docs
API

Citations and documents

The sources an answer rests on, OpenAI url_citation annotations, and downloading a member's documents by handle.

An answer can point at two kinds of material:

  • citations: passages of PlanCover's policy knowledge base that the answer rests on, marked [1], [2]… in the text;
  • documents: the member's own files, such as an e-card or a policy document, offered for download through a short-lived handle.

Both arrive in the plancover object: on the final chunk of a stream, or on a non-streaming completion.

Citations

{
  "index": 1,
  "id": "chunk-gmc-14",
  "title": "Group Medical Cover — Maternity — p.12",
  "document_id": "gmc-2026",
  "url": "https://docs.plancover.example/kb/gmc-2026",
  "snippet": "Maternity benefits are payable after a waiting period of nine months…"
}
FieldTypeMeaning
indexintegerThe n of the [n] marker in the answer
idstringThe passage's id
titlestringA label for the passage: document, section, page
document_idstringThe knowledge-base document the passage comes from
urlstring or nullA link to that document, when the deployment has one configured
snippetstring or nullA short excerpt of the passage

Every key is always present. Only sources the answer actually used are listed: a citation that does not match evidence found for this turn is dropped rather than sent. An answer with no sources has "citations": [].

To render them, replace each [n] in the text with a link or a chip for the citation whose index is n. On a stream, citations arrive only on the final chunk, after the text.

Annotations

A non-streaming completion also carries OpenAI-style annotations on choices[0].message.annotations, so clients that already render OpenAI citations work unchanged:

{
  "type": "url_citation",
  "url_citation": {
    "url": "https://docs.plancover.example/kb/gmc-2026",
    "title": "Group Medical Cover — Maternity — p.12",
    "start_index": 42,
    "end_index": 45
  }
}
  • One annotation per citation that has a URL and whose [n] marker is in the text. A citation without a URL gets none, because a partial annotation makes @ai-sdk/openai reject the whole response.
  • start_index and end_index span the marker's first occurrence, so content[start_index:end_index] is "[1]".
  • annotations is [] when no citation qualifies.
  • Streams carry no annotations. Use plancover.citations.

plancover.citations is the complete list. Annotations are a convenience subset of it.

Document handles

When the member asks for their e-card or policy documents, the answer names them, and plancover.documents lists them:

{ "documents": [{ "name": "E-card", "handle": "6741414141414141…" }] }
FieldMeaning
nameThe document's name, to show the member
handleAn opaque, encrypted reference to the file. It is not a URL

Documents come only from a signed-in member's own data, so a turn without a signed-in session always has "documents": [].

Downloading a document

Your backend fetches the file with your key and the same member session the turn ran with, and passes the bytes on to the member:

curl "$BASE_URL/documents/$HANDLE" \
  -H "Authorization: Bearer $CHAT_API_KEY" \
  -H "X-PlanCover-Session: $SESSION" \
  -o ecard.pdf

The answer is the file itself, streamed, with its own Content-Type, Content-Disposition: attachment and Cache-Control: private, no-store. It is never a redirect: the file's address at PlanCover is never exposed.

StatuscodeWhen
401unauthorisedNo session, or a malformed one
403forbiddenThe handle has expired, was minted for another member, or is not a handle. All three get the same answer
503upstream_unavailableThe file could not be fetched from PlanCover right now, or declares itself too large to download

This route is not under /v1, so its errors use the native envelope.

Handles expire after five minutes

A handle works for DOCUMENT_HANDLE_TTL_SECONDS (300 seconds by default) from when the assistant fetched it, and only for the member it was minted for. Fetch it when the member clicks, or straight away; to get a fresh one, ask the assistant again. Never put a handle in a page URL or a log: proxy the download through your backend.

On this page