Skip to content
AI Product Engineer Wiki

    ↑ ↓ to move between results, Enter to open, Esc to close

    Menu

    Citations and showing sources

    Show where AI answers come from with citations, quoted passages and source links that let users verify claims, built with the Claude citations feature.

    Level: Intermediate 9 min read Updated
    On this page

    Why this matters

    An AI answer without sources asks users to take it on faith. Sometimes that’s fine: nobody needs a citation for a suggested subject line. But for anything users will act on, repeat to others or rely on later, “where did this come from?” is the first question they’ll ask.

    Citations answer it. They let users verify a surprising claim in seconds, they make wrong answers visible instead of hidden, and they shift trust from the model to the source, which is where it belongs. This article covers when to show sources, the patterns that work, and how to get reliable citations from the API.

    Why sources matter

    Showing sources does several jobs at once:

    • Verification. Users can check the claim that matters to them without re-reading everything.
    • Catching mistakes. When a cited passage doesn’t support the claim, the error is obvious. Without citations, the same error looks exactly like a correct answer.
    • Trust calibration. Users learn when to rely on the feature and when to check, instead of trusting everything or nothing.
    • Accountability. In regulated or high-stakes settings, being able to show what an answer was based on is often a requirement.

    There’s also a benefit for you: answers grounded in sources you provided are far less likely to contain invented facts, because the model is working from text in front of it rather than from memory. See what LLMs are good and bad at.

    Citation patterns

    Anatomy of a cited answer. Each layer gives users a faster way to check a claim.
    Show text descriptionHide text description

    4 parts, in order:

    1. Claim: A specific sentence in the answer Example: New hires get 25 vacation days.
    2. Marker: A small, clickable reference next to the claim Example: …25 vacation days. [1]
    3. Quoted passage: The exact text the claim is based on, shown on hover or tap Example: "Full-time employees accrue 25 days of paid leave per year."
    4. Source: Title and link to the full document, opened at the right place Example: Employee handbook › Leave policy

    How much of that to show depends on the stakes:

    Pattern What users see Good for
    Source list “Based on: Handbook, Leave policy” under the answer Low stakes, short answers
    Inline markers Numbered markers after claims, linking to sources Most question answering
    Quoted passages The exact supporting text, on hover or expanded Answers users act on or share
    Side-by-side Answer next to the document, with cited passages highlighted Review work: contracts, research, compliance

    Some practical rules:

    • Cite claims, not paragraphs. A marker after each specific claim is more useful than one list at the end.
    • Quote exactly. Show the passage the claim relies on, word for word, so users don’t need to open the document.
    • Link to the right place. Opening a 40-page document at page one defeats the purpose.
    • Show uncited claims honestly. If part of an answer has no source, don’t decorate it with one. A clear difference between cited and uncited text is itself useful information.

    Getting citations from the API

    You could ask a model to “include sources” in its text, but then the model writes the references itself, and nothing guarantees they’re accurate. Claude’s citations feature (opens in a new tab) is more reliable: you pass documents with citations enabled, and the response comes back split into text blocks, each carrying structured citations that point to the exact passage in your documents.

    This function answers a question from a set of documents and returns the answer with numbered markers and the quoted passages behind them:

    lib/answer-with-citations.ts
    import Anthropic from '@anthropic-ai/sdk';
    
    const client = new Anthropic();
    
    export interface Source {
      title: string;
      url: string;
      text: string;
    }
    
    export type CitedAnswer =
      | { status: 'answered'; text: string; citations: { n: number; title: string; url: string; quote: string }[] }
      | { status: 'declined' }
      | { status: 'incomplete'; reason: string };
    
    export async function answerWithCitations(question: string, sources: Source[]): Promise<CitedAnswer> {
      const response = await client.messages.create({
        model: 'claude-opus-5-5',
        max_tokens: 4096,
        output_config: { effort: 'low' },
        system: 'Answer using only the provided documents. If they do not contain the answer, say so plainly instead of guessing.',
        messages: [
          {
            role: 'user',
            content: [
              ...sources.map(
                (s): Anthropic.DocumentBlockParam => ({
                  type: 'document',
                  source: { type: 'text', media_type: 'text/plain', data: s.text },
                  title: s.title,
                  citations: { enabled: true },
                }),
              ),
              { type: 'text', text: question },
            ],
          },
        ],
      });
    
      if (response.stop_reason === 'refusal') return { status: 'declined' };
      // A cut-off answer may have lost the claims, or the citations, that matter most. Don't show it as complete.
      if (response.stop_reason !== 'end_turn') return { status: 'incomplete', reason: String(response.stop_reason) };
    
      let text = '';
      const citations: { n: number; title: string; url: string; quote: string }[] = [];
      const numberFor = new Map<string, number>();
    
      for (const block of response.content) {
        if (block.type !== 'text') continue;
        text += block.text;
        for (const citation of block.citations ?? []) {
          // Plain-text documents produce character-range citations.
          if (citation.type !== 'char_location') continue;
          const source = sources[citation.document_index];
          if (!source) continue;
          // Reuse the same number when two claims cite the same passage.
          const key = `${citation.document_index}:${citation.start_char_index}`;
          let n = numberFor.get(key);
          if (n === undefined) {
            n = citations.length + 1;
            numberFor.set(key, n);
            citations.push({ n, title: source.title, url: source.url, quote: citation.cited_text });
          }
          text += ` [${n}]`;
        }
      }
    
      return { status: 'answered', text, citations };
    }

    Your interface then renders text with the markers as links, and each entry in citations as a source card with its quote. Things to notice:

    • Citations are data, not text. Each one points to a real document by index and to an exact character range, and includes the quoted passage. Your code builds the markers; the model never types a reference.
    • The system prompt allows “not in the documents”. Without that instruction, a model may stretch a weak source to answer anyway.
    • Unfinished answers aren’t shown as complete. Refusals and truncated answers get their own states.
    • Citations don’t combine with structured outputs. If you need JSON as well, make a separate call or extract citations first. The citations docs (opens in a new tab) list the supported document types, including PDFs, which cite by page.

    Where the documents come from is a separate problem: a small set can be passed whole; a large one needs retrieval first. Project 2 combines both.

    When there is no good source

    The hardest citation case is when the answer isn’t in the sources. The right behavior is to say so, clearly and helpfully:

    • Say what’s missing. “The handbook doesn’t cover remote work outside your country” is more useful than a generic “I don’t know”.
    • Offer the next step. Link to the closest relevant section, suggest who to ask, or offer to search more widely.
    • Never fill the gap from the model’s memory without saying so. If you allow answers beyond the sources, label them clearly as uncited.

    Measure this behavior in your evals: include questions whose answers aren’t in the documents, and check that the feature declines to answer rather than inventing one. That single category of eval case catches many of the most damaging failures. Communicating confidence and uncertainty covers abstaining in more depth.

    Common mistakes

    • Asking the model to write its own references. Use structured citations from the API, so every marker points to a real passage.
    • One source list for a whole answer. Cite specific claims so users can check the one they care about.
    • Linking to the top of a long document. Show the quoted passage, and link to the right page or section.
    • Forbidding “I don’t know”. Allow the model to say the sources don’t answer the question, and design that state.
    • Never testing unanswerable questions. Include them in your eval set; they catch invented answers.

    Try it

    Exercise

    About 60 min

    Add citations to a question-answering feature

    1. Pick three or four documents: help articles, a handbook section, product docs.
    2. Use the function above to answer five questions, including two the documents don’t answer.
    3. Render the answer with clickable markers and a source card showing each quote.
    4. Check every citation: does the quoted passage actually support the claim next to it? Note any that don’t.
    Show a hint

    Start rendering with something simple: replace each [n] with a link to an anchor on the page, and list the sources with their quotes below the answer. You can make it prettier later.

    Show a worked solution

    What a good result looks like:

    • Answerable questions get answers with a marker after each specific claim, and each quote clearly supports its claim.
    • The two unanswerable questions get a clear “the documents don’t cover this” answer, ideally saying what is covered nearby.
    • If any citation doesn’t support its claim, that’s a finding: tighten the system prompt, improve the documents, or add the case to your eval set.

    Check your understanding

    Question 1 of 4: Why use the API's citations feature rather than asking the model to list its sources in the text?

    Show answer

    B. Each citation points to an exact passage in documents you supplied, so references cannot be invented

    Structured citations reference real document indexes and character ranges, with the quoted text included.

    Question 2 of 4: Users will repeat answers to customers. Which citation pattern fits best?

    Show answer

    C. Inline markers with quoted passages

    When users act on or repeat an answer, they need to verify specific claims quickly, which the quoted passage allows.

    Question 3 of 4: What should the feature do when the documents do not contain the answer?

    Show answer

    B. Say clearly that the documents do not cover it, and suggest a next step

    Saying what is missing, helpfully, is better than an invented answer or a misleading citation.

    Question 4 of 4: In the code, why does a max_tokens stop return "incomplete" instead of the partial answer?

    Show answer

    B. Because a cut-off answer may be missing key claims or citations and should not be presented as complete

    A partial answer shown as complete is misleading, especially when users rely on its sources.

    Key takeaways

    • Sources let users verify claims, catch mistakes and calibrate trust.
    • Choose the pattern by stakes: source lists, inline markers, quoted passages or side-by-side review.
    • Use structured citations from the API so every marker points to a real passage.
    • Design and test the “not in the sources” path: a clear, helpful “I couldn’t find this” builds trust.

    Progress is saved in this browser and lights up the roadmap.