Skip to main content

Citations

When a bot answers from your knowledge base, the answer text carries [Source N] markers and the response carries a citations array that maps each N to the document and passage it came from. Citations are an eCourtDate extension to the OpenAI response shape; clients that ignore them still get a valid OpenAI response.

{
"role": "assistant",
"content": "Filing fees can be paid online through the court's payment portal or in person at the clerk's office [Source 1]. A convenience fee of 2.5% applies to card payments [Source 2].",
"refusal": null,
"tool_calls": null,
"citations": [
{
"source_index": 1,
"document_id": "9b1d7c2e-4f6a-4a8e-b3d5-0c7e2f9a1b64",
"source_filename": "fee-schedule-2026.pdf",
"chunk_index": 4,
"text_preview": "Payment methods. Filing fees may be paid online through the payment portal, or in person at the Clerk of Court's office during business hours.",
"score": 0.94
},
{
"source_index": 2,
"document_id": "9b1d7c2e-4f6a-4a8e-b3d5-0c7e2f9a1b64",
"source_filename": "fee-schedule-2026.pdf",
"chunk_index": 5,
"text_preview": "A convenience fee of 2.5% is added to all credit and debit card payments.",
"score": 0.88
}
]
}

How grounding works

On each request the API takes the text of the last user message, embeds it, retrieves the most relevant passages (chunks) from the bot's knowledge base, and passes them to the underlying language model numbered [Source 1], [Source 2], and so on, with the instruction to cite them in that notation. After generation the API scans the complete answer for markers and builds one citation object for every distinct N that refers to a retrieved chunk.

Two consequences:

  • The markers are part of content and are always left in place, whether or not citations resolves them. Rendering them is your job; see below.
  • A marker is only as reliable as the model that wrote it. The API verifies that N refers to a retrieved chunk, not that the chunk supports the sentence. Show users the cited passage (text_preview) so they can check.

The citation object

FieldTypeDescription
source_indexintegerThe N in [Source N]. 1-based. Indexes are unique within one answer but a cited chunk may be any of the retrieved ones, so indexes need not be consecutive.
document_idstringId of the document the passage belongs to. Use it with GET /v1/documents/{documentId} to show the document's current details.
source_filenamestringThe document's filename: the stored filename for an uploaded file, or the page URL for a crawled page. May be an empty string when no name was recorded.
chunk_indexinteger0-based position of the passage within the document's chunks. Stable for the life of the document; a re-uploaded file is a new document with new chunks.
text_previewstringThe first 200 characters of the passage text.
scorenumberThe relevance score the retrieval stage assigned to the passage. Use it to order or filter citations; do not compare values across requests or treat it as a probability.

Where citations appear

SurfaceLocation
POST /v1/chat/completionschoices[0].message.citations, always present: an array or null (Chat completions)
POST /v1/chat/completions with stream: truechoices[0].delta.citations on the finish chunk only, an array when present (Streaming)
POST /v1/conversations/{conversationId}/messagesmessage.citations, an array or null (Conversations)
GET /v1/conversations/{conversationId}messages[].citations on each stored assistant message, an array or null; always null on user messages

In the OpenAI Python SDK the field is available as message.model_extra["citations"] (or delta.model_extra["citations"] on a stream chunk). The Node SDK passes unknown fields through, so message.citations works directly (cast the type if you use TypeScript).

When citations are absent

On a chat completion message and on a conversation message the citations key is always present. It is an array (possibly empty) on a text answer from a bot that has a knowledge base and citations enabled, and null otherwise. On a streamed chat completion the finish chunk carries the array in the same cases and omits the key otherwise.

citations is null when any of these holds:

  • The bot has no knowledge base. A bot configured without a namespace answers from general knowledge and never cites.
  • Citations are disabled on the bot. include_citations is a bot-level setting configured by eCourtDate for your account (default: enabled). When it is off, [Source N] markers may still appear in content but citations is null.
  • The turn is a tool call. A response whose finish_reason is tool_calls has citations: null, even on a citing bot.

citations is an empty array, on a citing bot, when any of these holds:

  • Retrieval returned nothing for the query, or the answer contains no valid marker. A marker whose N is outside the retrieved range is ignored (no citation is created for it).
  • The last message is a tool result. Retrieval is not run on that turn, so the text answer that follows a tool call cites nothing. Citations are computed again on the next user message.

Code that reads citations should treat null and an empty array the same way.

Rendering citations

Wait for the complete answer (the non-streaming response, or the finish chunk of a stream) before touching the markers: [Source N] can be split across stream deltas. Then replace each marker with a footnote or link built from the matching citation object.

import re

MARKER = re.compile(r"\[Source (\d+)\]")

def render(message: dict) -> str:
by_index = {c["source_index"]: c for c in message.get("citations") or []}
footnotes = []

def replace(match: re.Match) -> str:
n = int(match.group(1))
citation = by_index.get(n)
if citation is None:
return "" # marker without a citation: drop it
if citation not in footnotes:
footnotes.append(citation)
return f"[{footnotes.index(citation) + 1}]"

body = MARKER.sub(replace, message["content"]).strip()
notes = "\n".join(
f"[{i}] {c['source_filename'] or c['document_id']}: \"{c['text_preview']}\""
for i, c in enumerate(footnotes, start=1)
)
return f"{body}\n\n{notes}" if notes else body

Applied to the example at the top of this page:

Filing fees can be paid online through the court's payment portal or in person at the clerk's office [1]. A convenience fee of 2.5% applies to card payments [2].

[1] fee-schedule-2026.pdf: "Payment methods. Filing fees may be paid online through the payment portal, or in person at the Clerk of Court's office during business hours."
[2] fee-schedule-2026.pdf: "A convenience fee of 2.5% is added to all credit and debit card payments."

Guidance that holds up in court-facing products:

  • Always show the source. Display source_filename (falling back to document_id when it is empty) and text_preview so staff and the public can verify an answer against the actual notice, rule, or schedule.
  • Renumber for display. source_index reflects retrieval order, not reading order; renumber in order of first appearance, as above.
  • Deep-link when you can. If your system maps document_id to a URL in your own document store, link the footnote there. The API does not return source URLs in the citation object.
  • Drop orphan markers. A [Source N] with no matching citation (for example when citations are disabled on the bot) should be removed rather than shown to users.
  • Keep markers when replaying history. When you send an earlier assistant reply back in messages, send its content as returned. The markers are harmless there, and the citations field is ignored on input.

Retrieval scope

What a bot can cite is fixed by its configuration, which eCourtDate sets up for your account:

  • Namespace. Each bot retrieves from exactly one namespace of your knowledge base. Files are assigned to a namespace when you upload or crawl them (default general). Content in another namespace is invisible to the bot.
  • Document filters. A bot can be restricted to a fixed list of document ids within its namespace, for example a "fee schedule" bot that only ever cites the current fee schedule.
  • Passage count. The number of passages handed to the model per request is configured per bot.

Deleting a document removes its chunks from retrieval immediately; answers already returned keep their document_id references, which will then resolve to 404 on GET /v1/documents/{documentId}. See Documents.