Claude Citations — empty citations, wrong offsets, or missing grounding
Citations are Claude's built-in RAG grounding mechanism — the model returns not just an answer but pointers to the exact source spans it used. When they fail to appear, the ground truth silently disappears too.
Quick fix (TL;DR)
citations arrays to text blocks in the response, each with cited_text, document_index, and character offsets. Requires (a) documents provided via document content blocks with citations.enabled: true, (b) the correct beta header, and (c) sensible source formatting. Fix empty citations by structuring documents in the expected shape, and use plain-text or PDF source types — not raw string prompts.Real error messages you'll see
These are the exact strings returned by the Claude API service and its SDKs when this error occurs. Copy-paste-searching any of them should land on this page.
TextBlock(
type="text",
text="The Q3 revenue was $4.2M.",
citations=[] # ← empty — grounding not attached
)anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': "'citations' is not supported without the "
"'citations-2025-01-14' beta header."}}anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'citations.enabled requires source.type to be text or content, not base64.'}}
Reference
Document block shapes that support citations
| Source type | Citations support | Best for |
|---|---|---|
{"type": "text", "media_type": "text/plain", "data": "..."} | Yes | Plain-text sources |
{"type": "content", "content": [...]} | Yes — chunked content blocks | Pre-chunked documents |
{"type": "file", "file_id": "..."} | Yes (PDF/text) | Files API uploads |
{"type": "base64", "media_type": "application/pdf", "data": "..."} | Yes (with beta) | Inline PDFs |
Citation object structure
| Field | Type | Meaning |
|---|---|---|
type | string | char_location | page_location | content_block_location |
cited_text | string | Exact substring from the source |
document_index | int | Which document (0-based) in the messages array |
document_title | string | Title attached at upload |
start_char_index | int | Offset in the source (text sources) |
end_char_index | int | Offset in the source (text sources) |
start_page_number / end_page_number | int | PDF page number range |
Root causes, ranked by frequency
Based on developer reports across Claude API forums, GitHub issues, and Anthropic community during 2025–2026.
- 26%citations.enabled not set on the document block. Simply providing a document without opting in gives grounding but no citation objects.
- 18%Beta header missing. Citations require the citations beta header on the request.
- 14%Documents provided as system prompt string instead of user message content. Citations only attach to content blocks in messages, not text embedded in the system prompt.
- 10%Answer generated from parametric knowledge, not the documents. Model chose to answer from its training rather than the provided docs — citations empty because none apply.
- 8%Documents provided but the answer paraphrases too heavily. When Claude cannot pinpoint a source span, it omits the citation. Better prompting can encourage tighter grounding.
- 7%Source encoding issue. Non-UTF8 text sources or PDFs with unusual encoding cause offset mismatches that surface as empty
cited_text. - 7%Multiple documents but only one cited. Model latched onto one document; others are ignored despite relevance. Symptom of poor document ordering or overlong context.
- 10%Streaming client discards citations. Some SDK versions drop the citations sub-object from the streamed content_block. Update to a version that surfaces them.
Fixes — copy-paste solutions
Enable citations on document blocks with the beta header
Set the beta header on the client, provide documents as document content blocks in the user message, and mark each with citations.enabled: true. Every referenced source needs the flag.
import anthropic client = anthropic.Anthropic( default_headers={"anthropic-beta": "citations-2025-01-14"} # verify current beta ) def ask_with_citations(question: str, sources: list[dict]) -> anthropic.types.Message: """ sources: [{"title": "Q3 2025 earnings", "text": "..."}] """ content = [] for src in sources: content.append({ "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": src["text"], }, "title": src["title"], "citations": {"enabled": True}, # opt-in per document }) content.append({"type": "text", "text": question}) return client.messages.create( model="claude-opus-4-7", max_tokens=1000, system="Answer strictly from the provided sources. Cite every claim.", messages=[{"role": "user", "content": content}], ) response = ask_with_citations( "What was Q3 revenue and headcount?", [ {"title": "Q3 2025 earnings", "text": "Revenue in Q3 2025 was $4.2M ..."}, {"title": "Ops report Q3", "text": "Total headcount at end of Q3 was 47 ..."}, ], ) for block in response.content: if block.type == "text": print(block.text) for cite in block.citations or []: print(f" [Source #{cite.document_index}: '{cite.cited_text}']")
citations.enabled: true is mandatory. Sources without the flag can still influence the answer but produce no citation objects.Chunk long documents into content blocks for finer-grained citations
Instead of one huge text source, provide many content-typed document blocks — each a paragraph or section. Citations then point to the specific chunk rather than a huge offset range.
import anthropic from typing import Iterable client = anthropic.Anthropic( default_headers={"anthropic-beta": "citations-2025-01-14"} ) def chunked_document(chunks: Iterable[str], title: str) -> dict: """Wrap a list of chunks as a content-typed document with citations.""" return { "type": "document", "source": { "type": "content", "content": [ {"type": "text", "text": chunk} for chunk in chunks ], }, "title": title, "citations": {"enabled": True}, } # Build a chunked document from paragraphs paragraphs = [ "The company was founded in 2018.", "Q3 2025 revenue was $4.2M, up 18% YoY.", "Headcount at end of Q3 was 47.", "The board consists of five directors...", ] doc = chunked_document(paragraphs, title="Q3 2025 earnings") response = client.messages.create( model="claude-opus-4-7", max_tokens=500, system="Answer from the provided document. Every fact needs a citation.", messages=[{"role": "user", "content": [ doc, {"type": "text", "text": "What was Q3 revenue?"}, ]}], ) for block in response.content: if block.type != "text": continue print(block.text) for cite in block.citations or []: # For content-typed sources, cite includes block start/end indices print(f" [doc #{cite.document_index}, block " f"{cite.start_block_index}-{cite.end_block_index}: '{cite.cited_text}']")
Force grounded responses with prompt engineering + strict validation
Ask explicitly for cited claims in the system prompt, then verify at the client that every text block contains at least one citation. Reject and retry ungrounded responses.
import anthropic client = anthropic.Anthropic( default_headers={"anthropic-beta": "citations-2025-01-14"} ) STRICT_SYSTEM = """ You answer strictly from the provided documents. Rules: 1. Every factual claim must have a citation. 2. If the documents do not contain the answer, say so exactly: "The provided sources do not contain this information." 3. Do NOT use your parametric knowledge — only the documents. """ def call_with_strict_grounding(question: str, docs: list[dict], max_attempts: int = 2) -> anthropic.types.Message: for attempt in range(1, max_attempts + 1): content = [ { "type": "document", "source": {"type": "text", "media_type": "text/plain", "data": d["text"]}, "title": d["title"], "citations": {"enabled": True}, } for d in docs ] + [{"type": "text", "text": question}] response = client.messages.create( model="claude-opus-4-7", max_tokens=1000, system=STRICT_SYSTEM, messages=[{"role": "user", "content": content}], ) # Verify: every text block with non-trivial content has ≥1 citation # or explicitly says the docs do not contain the answer acceptable = True for block in response.content: if block.type != "text": continue text = block.text.strip() if not text: continue has_citations = bool(block.citations) disclaims = "provided sources do not contain" in text.lower() if not (has_citations or disclaims): acceptable = False break if acceptable: return response print(f"[attempt {attempt}] response contains ungrounded claim, retrying") return response # last attempt, even if imperfect
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Enable citations with the beta header and per-document
citations.enabled: true— feature is opt-in on both sides. - Provide sources in user messages as document blocks, not embedded in the system prompt.
- Chunk long documents into content-typed blocks (100-500 tokens each) for precise citations.
- Verify at the client that every text block has ≥1 citation (or an explicit no-source disclaimer).
- Log citation counts per response as a grounding-quality SLI; investigate when it drops.
- When streaming, use an SDK version that surfaces per-chunk citations objects.
- For high-stakes use cases (legal, medical), retry ungrounded responses rather than serving them.
Frequently asked questions
start_page_number and end_page_number ranges (page-based). Plain-text sources return start_char_index / end_char_index. Content-typed sources return block indices.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.