Developers

Extract PDF Tables With the Claude API: A Method That Holds Up

DC
DataConvertPro
~9 min read

You wrote the obvious version already: pass the PDF to the Messages API, ask for the table as JSON, get back something that parses cleanly, ship it. Then someone downstream noticed the reconciliation was off by four rows, and you had no way to tell which four or why. That is the real problem with LLM table extraction. It does not fail loudly, it fails short.

Short Answer

Send each page as its own request with the page image plus the extracted text, constrain the output with a JSON schema using structured outputs, and assert an expected row count that you compute independently of the model. The Claude API already converts each PDF page to an image and pairs it with the page text, so visual structure like merged cells and rule lines is available to the model. The assertion is the part most people skip, and it is the part that catches silent row loss.

Which approach fits your document

Approach Handles merged cells Handles multi-page tables Silent row loss risk Best for
Deterministic parser (pdfplumber, Camelot) Poorly, splits or blanks them No, each page parsed separately Low, output is verifiable Clean digital PDFs with ruled tables and stable layout
Single Claude API call, whole PDF, free-form JSON Yes Partially High Prototypes and one-off lookups
Per-page Claude call, page image plus schema plus row assertion Yes Yes, with an explicit continuation flag Low, failures surface as assertion errors Production pipelines over varied PDFs
Deterministic parser first, model only on pages that fail Yes on the hard pages Yes Low High-volume runs where cost per page matters

If your PDFs come from one generator and the tables are ruled and consistent, use pdfplumber or Camelot and stop reading. A deterministic parser you can unit test beats a model you have to trust. The model earns its cost when layout varies, when cells merge, or when the source is scanned.

Have a messy PDF? Upload 1-3 sample pages and we will tell you if it is clean, OCR-heavy, or needs human QA.

Why the page image matters

When you pass a document block to the Messages API, each page is converted to an image, the text of that page is extracted, and both are supplied to the model together. You are not choosing between OCR and text extraction. You get both.

This matters because the information that defines a table is largely visual. A merged cell spanning three rows is, in the text layer, one string followed by two absences. A row that continues onto the next page is, in the text layer, indistinguishable from a new row. Rule lines and column alignment are how a human reads those structures, and the page image is how the model sees them.

The cost side is documented. Text runs roughly 1,500 to 3,000 tokens per page depending on density. Image tokens follow a 28-by-28 pixel patch grid, so visual tokens land near ceil(width/28) * ceil(height/28), and images above the model's tier limit are downscaled automatically with aspect ratio preserved. That downscale is a trap for tables set in small type: the request succeeds, the token count looks reasonable, and the six-point column is now unreadable. Render your own page images at a resolution you have checked by eye.

The per-page loop

Anthropic documents 100 pages per request under a standard context window, rising to 600 pages with a 1M-token context, and a 32 MB request ceiling. The docs also warn that dense pages with complex tables can exhaust the context window well before you reach the page limit.

Do not build to those limits. Process one page per request, or a two-page window when tables span breaks. Attention does not stay uniform across 80 pages of similar-looking tables, so middle rows get compressed or skipped with nothing in the response to flag it. A failed page becomes a retry of one page rather than the document. And row-count assertions only work at a granularity where you already know the answer: you can count rows on one page, not across 80.

For the two-page window, pass page N and page N+1 and include a boolean like first_row_continues_previous_page in your schema. Reconcile at merge time instead of asking the model to hold document-level state it does not have.

Constrain the output with a schema

Use structured outputs rather than asking for JSON in the prompt. The output_config.format parameter constrains the response to your schema, and strict: true on a tool definition does the same for tool inputs. Both are supported on current Claude models without a beta header.

Two constraints matter before you design the schema. Regex pattern is not supported, and neither are minimum, maximum, minLength, or maxLength, though the SDKs fold those into field descriptions. There are also documented complexity ceilings: 20 strict tools, 24 total optional parameters, 16 parameters using union types. Keep row schemas flat. A row object with eight typed fields compiles fine. A deeply nested schema with optional unions everywhere hits "Schema is too complex for compilation."

Design the schema so absence is explicit. Every column gets a required field. A blank cell becomes an empty string, not a missing key. A merged cell spanning rows gets a carried_from_above boolean rather than a silent forward-fill, because forward-fill is a decision you want to make in your own code, not one the model makes inconsistently across pages.

The row-count assertion

This is the load-bearing step. Ask for the count in a way that does not depend on the array you are validating.

Add two fields to your page schema: visible_data_row_count, reported before the model enumerates rows, and rows, the array itself. Assert len(rows) == visible_data_row_count in your code. When the model truncates, drops near-identical rows, or collapses a merged block, the two numbers diverge. On divergence, retry at higher image resolution. If it diverges twice, route the page to a human.

The independent count is not perfect. The model can miscount and misenumerate in the same direction. But the common failures are asymmetric: truncation and dedupe shorten the array without changing the count the model formed while looking at the page, so the check fires. Where a numeric column has a printed total, sum your extracted column and compare that too. Both checks are cheap relative to finding the error in a client's reconciliation.

Failure modes that drop rows without erroring

  • max_tokens truncation. A 60-row page can exceed a conservative output budget. With structured outputs you get an error or an incomplete stop reason rather than plausible JSON, which is why you should not hand-roll JSON parsing from free text.
  • Repeated rows collapsing. Twelve consecutive rows with the same description and different amounts sometimes come back as eight. The row-count assertion catches this. Nothing else will.
  • Continuation rows. A row broken across a page boundary gets counted twice as a partial on each page, or dropped entirely. The continuation flag plus a merge step handles it.
  • Merged header cells. A two-level header with a spanning parent produces column labels the model invents by concatenation, and they differ page to page. Pin column names in the schema and never let the model name them.
  • Auto-downscaled images. Check a rendered page at your chosen resolution before running 4,000 of them.
  • Citations and structured outputs are mutually exclusive. You cannot enable citations and use output_config.format in the same request. If you need page-level provenance for audit, run a second pass with citations enabled, or store page numbers yourself from the per-page loop.

What this costs and when to stop building

Per-page cost follows from token counts and published rates: a dense page is a few thousand input tokens plus visual tokens for the page image, with small structured output. The Batch API applies a 50 percent discount if 24-hour turnaround is acceptable, and cached prompt reads cost a fraction of base input price, which helps when your schema and instructions repeat across thousands of pages.

The build pays off when you process the same document family repeatedly and can amortize the eval harness. It does not pay off on a one-time job of a few hundred pages.

DataConvertPro is a managed service. Humans plus AI run this workflow, including the assertion failures that need a person to look at the page, and deliver checked Excel or CSV. If you are building it yourself, the assertion step is the one to copy.

Frequently Asked Questions

Can the Claude API read scanned PDFs without separate OCR?

Yes for the visual pass. Each page is converted to an image, so a scanned page is legible to the model even when the text layer is empty or garbage. Quality still depends on scan resolution and skew, and very poor scans benefit from a deskew and contrast pass first.

Should I send the whole PDF or one page at a time?

One page, or a two-page window when tables cross breaks. The documented limits allow far more, but per-page processing is what makes row-count assertions meaningful and keeps a single bad page from forcing a full retry.

How do I handle a table that spans 40 pages?

Extract per page with a continuation flag on the first row, then merge in your own code. Carry the header from the first page rather than re-extracting it, since repeated header parsing is a common source of column name drift.

Is a deterministic parser still better for some documents?

Often, yes. If your PDFs are digitally generated, ruled, and structurally consistent, pdfplumber or Camelot gives you output you can unit test. Use the model on the pages the parser fails, not on every page by default.

Filed underDevelopers

Ready to Convert Your Documents?

Stop wasting time on manual PDF to Excel conversions. Get a free quote and learn how DataConvertPro can handle your document processing needs with AI-assisted extraction and human verification.