Skip to content
Docs Security Sign in
PdfParse

PDF table extraction that keeps merged cells

Tables are where PDF parsing usually breaks. PdfParse detects every table on the page, rebuilds the grid with row and column spans, joins tables that continue across pages and returns each one as structured rows, cell coordinates and CSV in the same response.

Parse PDF
Printed financial report pages with dense tables fanned out next to a highlighter and a calculator

Test it on your hardest table

Upload the table that broke your last parser and check the result on the tables tab, cell by cell, against the rendered page.

Drop a PDF or scan

PDF, PNG or JPG up to 10 MB. Page one is parsed in full, right here.

Or try a sample file
Output

Optional. Comma separated field names, for example invoice_number, total, due_date.

Page one is parsed in your browser and never uploaded. No card needed to try page one.

What one call returns

Page one rendered with every block outlined, next to the JSON your code receives.

  1. Reading text layer
  2. Detecting layout
  3. Extracting tables
  4. Building JSON
Rendered page Page 1 of your document

Headings, paragraphs, tables and totals detected

heading
Invoice
paragraph
Harbor Street Joinery
14 Dock Lane
Portsmouth, NH 03801
key_value
Invoice no.INV-2026-0418IssuedOct 14, 2026DueNov 13, 2026
table
DescriptionQtyUnitAmount
Oak countertop, 2.4 m2640.001,280.00
Cabinet doors, shaker1285.001,020.00
On site fitting1450.00450.00
total
Subtotal2,750.00Tax 8.5%233.75Total dueUSD 2,983.75
paragraph
Payment by bank transfer within 30 days.
Quote the invoice number with your transfer.
Goods remain ours until paid in full.
footer
Harbor Street JoineryPage 1 of 2
Locked page

Pages 2 and later are parsed after you create an account.

Output
{
  "document": {
    "pages": 2,
    "parsed_pages": 1,
    "language": "en"
  },
  "pages": [
    {
      "number": 1,
      "blocks": [
        { "type": "heading", "text": "Invoice", "bbox": [48, 52, 132, 84] },
        { "type": "paragraph", "text": "Harbor Street Joinery, 14 Dock Lane, Portsmouth" },
        { "type": "key_value", "pairs": {
            "Invoice no.": "INV-2026-0418",
            "Issued": "Oct 14, 2026",
            "Due": "Nov 13, 2026" } },
        { "type": "table", "columns": ["Description", "Qty", "Unit", "Amount"],
          "rows": [
            ["Oak countertop, 2.4 m", 2, 640.00, 1280.00],
            ["Cabinet doors, shaker", 12, 85.00, 1020.00],
            ["On site fitting", 1, 450.00, 450.00] ] },
        { "type": "total", "label": "Total due", "value": 2983.75, "currency": "USD", "confidence": 0.98 }
      ]
    }
  ],
  "fields": {
    "invoice_number": { "value": "INV-2026-0418", "confidence": 0.99, "page": 1 },
    "total": { "value": 2983.75, "confidence": 0.98, "page": 1 },
    "due_date": { "value": "2026-11-13", "confidence": 0.97, "page": 1 }
  }
}

Downloads from the demo include a watermark field and cover page one only.

Page one is parsed in your browser, so your file never reaches our servers.

Request
The same request from your code
Read the API reference
$ curl https://api.pdfparse.ai/v1/parse \
  -H "Authorization: Bearer $PDFPARSE_API_KEY" \
  -F file[email protected] \
  -F outputs=json,markdown,csv
200 OK, 1 page parsed

A table object with real cell structure

Each detected table is described twice. A rows array gives you the cells in order, with the span of every cell and its position on the page. A CSV string gives you the flattened version for spreadsheets. Both come from one pass, so they always agree.

  • Row and column counts for every table
  • Cells with text, row span, column span and a bounding box
  • Header rows flagged separately from body rows, including two and three level headers
  • The page number, plus a continuation flag when the table runs onto the next page
  • Totals and subtotal rows kept as rows, never merged into the header
JSON
{
  "page": 3,
  "rows": 4,
  "columns": 4,
  "cells": [
    {"row": 0, "col": 0, "text": "Region", "row_span": 2, "col_span": 1, "header": true},
    {"row": 0, "col": 1, "text": "Revenue", "row_span": 1, "col_span": 3, "header": true},
    {"row": 1, "col": 1, "text": "Q1", "header": true},
    {"row": 2, "col": 0, "text": "North", "bbox": [72, 410, 160, 428]}
  ],
  "csv": "Region,Revenue Q1,Revenue Q2,Revenue Q3\nNorth,410,455,498"
}

How tables are found and rebuilt

  1. 1

    The page is rendered and scanned for table regions, using ruling lines, whitespace and repeated alignment.

  2. 2

    Cell boundaries are fixed, and cells that cover more than one row or column get their span.

  3. 3

    Header rows are identified, and stacked headers are combined into one name per column for the CSV.

  4. 4

    Parts of a table that continue on later pages are matched by column layout and joined.

  5. 5

    Everything is returned in the tables array, with the text of each table also present in the page blocks.

The cases we test against

Simple grids with every line drawn are easy. The tables people send us are rarely like that, so the engine is tuned for the awkward ones.

  • Merged cells in headers and in the first column, as in financial statements grouped by region or segment.
  • Tables that break across three or four pages with a repeated header, joined into one table.
  • Borderless tables aligned only by spacing, and tables printed with zebra shading instead of lines.
  • Scanned tables where OCR reads the digits first and the grid is rebuilt from their positions.
  • Landscape tables rotated inside a portrait document, and tables placed in one column of a two column page.

Pull every table from a document

Request json and csv together to get both the cell structure and the flat copy. Use pages to limit the call to the section that holds the tables.

curl
curl https://api.pdfparse.ai/v1/parse \
  -H "Authorization: Bearer pp_live_your_key" \
  -F "[email protected]" \
  -F 'outputs=["json","csv"]' \
  -F "pages=40-60"
Python
import os
from pdfparse_ai import PdfParse

client = PdfParse(api_key=os.environ["PDFPARSE_API_KEY"])
result = client.parse("10k-filing.pdf", outputs=["json", "csv"], pages="40-60")

for table in result.tables:
    print(f"page {table['page']}: {table['rows']} rows x {table['columns']} columns")

Where table extraction matters most

  • Financial analysts and research teams extracting statements and segment tables from filings.
  • Insurance and claims teams reading schedules of benefits and coverage tables.
  • Supply chain teams reading packing lists, bills of materials and rate cards.
  • Engineers building search or LLM features that must answer questions from numbers inside tables.

Plans that include table extraction

Table extraction with merged cells and multi page joins is part of every plan, from Starter at $49 a month, or $24 a month billed yearly, with 2,000 pages and files up to 100 pages. Growth at $149 a month raises the limit to 7,000 pages and 300 pages per file, which suits long filings, and adds batch jobs and webhooks. Scale at $499 a month handles files up to 1,000 pages. Details on pdf extraction api pricing.

Pricing
  • Starter 2,000 pages, 1 API key, 1 seat $49 per month $24 per month, $288 billed yearly
  • Growth 7,000 pages, 5 API keys, 3 seats $149 per month $74 per month, $888 billed yearly
  • Scale 24,000 pages, 20 API keys, 10 seats $499 per month $249 per month, $2,988 billed yearly
  • Enterprise 72,000 pages, unlimited API keys with per key limits, 25 seats $1,499 per month $749 per month, $8,988 billed yearly

FAQ

How are merged cells represented?

Each cell carries a row span and a column span in the JSON. In the CSV the merged value is repeated under each column it covers, so every row stays complete.

Can it join a table that continues on the next page?

Yes. Parts with the same column layout on consecutive pages are joined into one table, and the repeated header is dropped from the joined rows.

Does it work on scanned tables?

Yes. With ocr set to auto, scanned pages are read first and the grid is rebuilt from the positions of the recognized words.

Can I tell which table a value came from?

Every table has a page number and every cell has a bounding box, so you can trace a value back to the exact spot on the original page.

What if a page has several tables?

Each one is returned as its own entry in the tables array, in reading order, with its own rows, columns and CSV.

Extract your first table

Create an account and send the document with the table nobody wanted to retype.