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
contentand are always left in place, whether or notcitationsresolves them. Rendering them is your job; see below. - A marker is only as reliable as the model that wrote it. The API verifies
that
Nrefers 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
| Field | Type | Description |
|---|---|---|
source_index | integer | The 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_id | string | Id of the document the passage belongs to. Use it with GET /v1/documents/{documentId} to show the document's current details. |
source_filename | string | The 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_index | integer | 0-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_preview | string | The first 200 characters of the passage text. |
score | number | The 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
| Surface | Location |
|---|---|
POST /v1/chat/completions | choices[0].message.citations, always present: an array or null (Chat completions) |
POST /v1/chat/completions with stream: true | choices[0].delta.citations on the finish chunk only, an array when present (Streaming) |
POST /v1/conversations/{conversationId}/messages | message.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_citationsis a bot-level setting configured by eCourtDate for your account (default: enabled). When it is off,[Source N]markers may still appear incontentbutcitationsisnull. - The turn is a tool call. A response whose
finish_reasonistool_callshascitations: 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
Nis outside the retrieved range is ignored (no citation is created for it). - The last message is a
toolresult. Retrieval is not run on that turn, so the text answer that follows a tool call cites nothing. Citations are computed again on the nextusermessage.
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 todocument_idwhen it is empty) andtext_previewso staff and the public can verify an answer against the actual notice, rule, or schedule. - Renumber for display.
source_indexreflects retrieval order, not reading order; renumber in order of first appearance, as above. - Deep-link when you can. If your system maps
document_idto 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 itscontentas returned. The markers are harmless there, and thecitationsfield 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.