Read PDFs with Claude
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:
| Source | Use it when |
|---|---|
url | The PDF is public and will not change |
base64 | The PDF is a one-off, or comes from a user upload |
file_id | The 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
check_pdf()rejects non-PDF content, and files over the local size policy, before any request is made.- The document block uses
application/pdfas its media type, and the base64 source, exactly as the documentation shows. - The question comes after the document, so the request reads as "here is a document, now answer about it".
max_tokens=2048allows a longer answer than a single sentence, since questions about documents often need summaries or lists.- 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
- Run
read_pdf.pyon a text file renamed to.pdf. Which error appears, and at what point? - Ask for a page-by-page summary of a multi-page PDF. What does the answer contain, and what is missing?
- Change the question to request quotes with page references. Does the answer include verifiable source text?
- Explain why the signature check happens before the size check.
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.