Read PDFs with Claude

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 110 of 200

Sending a PDF as a Document

PDFs are sent as document content blocks, not image blocks. Claude processes each page using its vision capabilities, so it can read text, tables, charts, and scanned pages that contain no text layer at all. Because the pages are processed visually, a scanned contract and a digitally created report are handled the same way.

{
    "type": "document",
    "source": {
        "type": "base64",
        "media_type": "application/pdf",
        "data": "<base64 text>",
    },
}

As with images, the document is one content block in a user message, followed by a text block with the question:

messages = [{"role": "user", "content": [
    document_block,
    {"type": "text", "text": "Summarize the key terms of this contract in five bullet points."},
]}]

Three Ways to Provide a PDF

The current PDF documentation describes three source types, matching the image sources from Lessons 111–113:

SourceUse it when
urlThe PDF is public and will not change
base64The PDF is a one-off, or comes from a user upload
file_idThe same PDF is used in several requests (Lesson 116)

The documentation also notes that PDF limits apply to the entire request payload, including any other content sent with it. For large PDFs, uploading through the Files API and referencing the file ID keeps request payloads small. Check the PDF support page for the current size and page limits before relying on specific numbers, because they can change.


Checking the File Before Sending

As with images, check the bytes first. A PDF begins with the signature %PDF-, and a file that does not is not a PDF, whatever its name says:

PDF_SIGNATURE = b"%PDF-"
MAX_LOCAL_PDF_BYTES = 20 * 1024 * 1024   # Our own policy for this application, not an API limit


def check_pdf(data: bytes) -> None:
    if not data.startswith(PDF_SIGNATURE):
        raise ValueError("the file is not a PDF (its contents do not start with %PDF-)")
    if len(data) > MAX_LOCAL_PDF_BYTES:
        raise ValueError(f"PDF is {len(data) // (1024 * 1024)} MB; this application accepts up to 20 MB")

The size limit here is a local policy, chosen to keep this application's requests predictable and affordable. It is not the API's limit, so set it deliberately for your use case.


Practical Example: Asking Questions About a PDF

This example checks a PDF, builds the document block, and asks a question. It reads the file path from the command line, so you can try it with any PDF.

"""read_pdf.py: check a PDF and ask Claude a question about it.

Usage:
    python read_pdf.py report.pdf "What is the total revenue?"
"""

import sys
from pathlib import Path

from anthropic import Anthropic

from media_utils import encode_base64

client = Anthropic()
PDF_SIGNATURE = b"%PDF-"
MAX_LOCAL_PDF_BYTES = 20 * 1024 * 1024


def check_pdf(data: bytes) -> None:
    if not data.startswith(PDF_SIGNATURE):
        raise ValueError("the file is not a PDF (its contents do not start with %PDF-)")
    if len(data) > MAX_LOCAL_PDF_BYTES:
        raise ValueError(f"PDF is {len(data) // (1024 * 1024)} MB; this application accepts up to 20 MB")


def ask_about_pdf(data: bytes, question: str) -> str:
    check_pdf(data)
    message = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=2048,
        messages=[{"role": "user", "content": [
            {"type": "document", "source": {
                "type": "base64",
                "media_type": "application/pdf",
                "data": encode_base64(data),
            }},
            {"type": "text", "text": question},
        ]}],
    )
    return "".join(block.text for block in message.content if block.type == "text").strip()


if __name__ == "__main__":
    path, question = Path(sys.argv[1]), " ".join(sys.argv[2:])
    print(ask_about_pdf(path.read_bytes(), question))

Example run (the answer depends on the document):

$ python read_pdf.py report.pdf "What is the total revenue?"
Total revenue for the year is ₹4.2 crore, up 12% from the previous year.

How It Works

  1. check_pdf() rejects non-PDF content, and files over the local size policy, before any request is made.
  2. The document block uses application/pdf as its media type, and the base64 source, exactly as the documentation shows.
  3. The question comes after the document, so the request reads as "here is a document, now answer about it".
  4. max_tokens=2048 allows a longer answer than a single sentence, since questions about documents often need summaries or lists.
  5. The command line makes the example easy to try with any PDF, including scanned ones.

When Text Extraction Is Enough

Sending a PDF to Claude is not always the best choice. A PDF that is already plain text, such as a text export, is cheaper and simpler to send as text (Lesson 117). Use the PDF path when layout, tables, images, or scanned pages matter. Text extraction can also be useful before sending, for example to count tokens and decide how to split a long document (Lessons 46 and 118).


Common Mistakes

Sending a File Without Checking Its Signature

What happens: a renamed file, such as a Word document named .pdf, is sent as a PDF and rejected or misread.

Fix: check the %PDF- signature first.

Treating the Local Size Policy as an API Limit

What happens: the application rejects PDFs that the API would accept, or accepts ones it rejects.

Fix: keep the local policy separate, and check the current documented limits.

Forgetting the Whole-Request Limit

What happens: a PDF that fits alone fails once a large image or long text is added to the same request.

Fix: count the whole request, and use the Files API for large documents.

Expecting Perfect Reading of Dense Pages

What happens: very small print, handwriting, or low-quality scans produce errors.

Fix: verify important values, and ask Claude to quote the source text for each answer (Lesson 33).


Best Practices

  • Check the signature before sending.
  • Keep local policies separate from API limits.
  • Count the whole request, not just the document.
  • Upload large or repeatedly used PDFs through the Files API (Lessons 115–116).
  • Ask for quotes or page references when accuracy matters.
  • Use text where text is enough, and the PDF path where layout matters.

Exercise

  1. Run read_pdf.py on a text file renamed to .pdf. Which error appears, and at what point?
  2. Ask for a page-by-page summary of a multi-page PDF. What does the answer contain, and what is missing?
  3. Change the question to request quotes with page references. Does the answer include verifiable source text?
  4. Explain why the signature check happens before the size check.
Show answers for steps 1 and 4

Step 1: check_pdf() raises ValueError before any request is sent, because the content does not begin with %PDF-. The message explains the problem, and no tokens are spent. This is the point of checking locally first.

Step 4: a file with the wrong content can be small or large. Rejecting it on its content first gives the most useful message, "this is not a PDF", rather than a size message that would be misleading for a file that was never a PDF in the first place. Checks run from the most fundamental property to the least, so each failure explains the real problem.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Read PDFs with Claude and get answers drawn from it.

Signed-in readers only.