# PDF Tables to CSV and JSON (`kiwitools/pdf-tables-to-csv-json`) Actor

Extract tables from batches of PDF URLs with page references and downloadable CSV and JSON files.

- **URL**: https://apify.com/kiwitools/pdf-tables-to-csv-json.md
- **Developed by:** [Kiwi Tools](https://apify.com/kiwitools) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 page processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PDF Tables to CSV and JSON

Turn the tables inside PDF reports into spreadsheet-ready **CSV** and structured **JSON**. Paste a list of PDF links, get one clean file per table, with the source URL and page number attached to every table.

Powered by the open-source Docling library for layout and table-structure recognition, with optional English OCR for scanned pages.

### What it's good for

- **Financial statements and annual reports**: income statements, balance sheets and cash-flow tables into CSV for analysis.
- **Government and statistics publications**: pull the data tables out of 50-page reports without copying by hand.
- **Research papers**: results tables from scientific PDFs, keeping multi-level headers readable.
- **Batch pipelines**: up to 100 PDFs per run via the API, with per-PDF status so one bad link never stops the batch.
- **Scanned documents**: turn on OCR for pages that have no text layer (see OCR caveats below).

### How it works

1. Each PDF is downloaded (public HTTP/HTTPS links only) and checked.
2. Docling detects tables and their cell structure on the first `maxPages` pages.
3. Tables are cleaned up:
   - Stacked headers become `Parent / Child` names, e.g. `Three Months Ended / September 28, 2024`.
   - Merged cells and section labels such as `ASSETS:` appear once, not repeated across every column.
   - Tables of contents and lists of figures are skipped.
4. Each table is saved as its own CSV and JSON file, and listed in the dataset with download links.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `pdfUrls` | array of URLs | required | Direct links to public PDFs, 1–100. Duplicates are processed once. |
| `maxPages` | integer | 20 | Process the first N pages of each PDF (1–200). You pay per processed page. |
| `enableOcr` | boolean | false | Read scanned pages (no text layer) with English OCR. |
| `tableMode` | `accurate` / `fast` | `accurate` | Table-structure model. `fast` is quicker on simple grids. |
| `headerStyle` | `joined` / `leaf` / `dotted` | `joined` | Multi-level headers as `Parent / Child`, lowest header only, or `Parent.Child`. |
| `mergedCells` | `first` / `repeat` | `first` | Put a merged cell's text in its first cell only, or repeat it in every covered cell. |
| `includeNonDataTables` | boolean | false | Keep contents pages and lists of figures (marked `tableType: "contents"`). |
| `safeCsv` | boolean | true | Prefix formula-like CSV cells (`=`, `+`, `-`, `@`) with an apostrophe so spreadsheets don't execute them. JSON keeps the original text. |
| `maxFileSizeMb` | integer | 25 | Reject larger downloads (1–100 MiB). |
| `documentTimeoutSeconds` | integer | 180 | Stop converting a PDF that takes longer than this (30–1800). |

```json
{
  "pdfUrls": ["https://example.org/annual-report.pdf", "https://example.org/statistics.pdf"],
  "maxPages": 20,
  "enableOcr": false,
  "headerStyle": "joined"
}
```

### Output

**Dataset**: one `table` record per extracted table and one `document` record per PDF. The Console's "Tables and document results" view shows the key fields with CSV/JSON links.

Table record (shortened):

```json
{
  "recordType": "table",
  "sourceUrl": "https://example.org/annual-report.pdf",
  "tableIndex": 1,
  "pages": [1],
  "columns": ["", "Three Months Ended / September 28, 2024", "Three Months Ended / September 30, 2023"],
  "rows": [["Net sales:", "", ""], ["Products", "$ 69,958", "$ 67,184"]],
  "rowCount": 38,
  "columnCount": 5,
  "headerRowCount": 2,
  "sectionRows": [0],
  "tableType": "data",
  "ocrUsed": false,
  "caption": "Table 1: …",
  "csvUrl": "https://api.apify.com/v2/key-value-stores/…/records/doc-001-…-table-001.csv?signature=…",
  "jsonUrl": "https://api.apify.com/v2/key-value-stores/…/records/doc-001-…-table-001.json?signature=…"
}
```

Document record (shortened):

```json
{
  "recordType": "document",
  "sourceUrl": "https://example.org/annual-report.pdf",
  "status": "success",
  "totalPages": 61,
  "convertedPages": 20,
  "tableCount": 4,
  "ocrPages": [],
  "nonDataTablesSkipped": [{"pages": [5], "reason": "dot leaders"}],
  "billing": {"events": {"page": 20, "ocr-page": 0}},
  "warnings": ["Only the first 20 pages were requested."]
}
```

`status` is one of:

- `success`
- `partial`: Docling converted only some pages.
- `error`: with `errorType` and `error`, e.g. HTTP 404, not a PDF, encrypted, corrupt, too large, or timed out.
- `skipped`: your maximum charge was reached.

**Key-value store**: `…-table-NNN.csv` (UTF-8 with BOM, opens directly in Excel) and `…-table-NNN.json` for every table, plus a `SUMMARY` record with totals and any skipped URLs.

### Pricing

Pay per event: you pay only for pages of PDFs that convert successfully.

| Event | Price | When it's charged |
| --- | --- | --- |
| Page processed (`page`) | $0.01 | Each page converted and searched for tables, including pages where no table was found. |
| OCR page processed (`ocr-page`) | $0.025 | Each scanned page (no text layer) read with OCR when `enableOcr` is on. Charged **instead of** a normal page, never both. |

- Failed, skipped and errored PDFs are free. Only the first `maxPages` pages are processed.
- Digital pages are always charged as normal pages, even with OCR enabled.
- Example: a batch of 10 reports × 20 pages costs $2.00. Five 2-page scans with OCR cost $0.25.
- Set **Maximum cost per run** in the run options. The Actor checks each PDF's page count before converting it. If processing it would go over your limit, it stops cleanly and lists the remaining URLs as `skipped` in the dataset and in `SUMMARY.skippedUrls`.

### OCR caveats (read before using scans)

- OCR reads pixels, not text. It **can silently misread numbers**: dropped decimal points (`1.47` → `147`), missing or wrong thousands separators (`294,866` → `294866`, `14,695` → `14.695`), and stray characters (`14,874 um`).
- In testing on a clean 200 dpi scan of financial statements, table structure was recovered exactly, but about 7% of cells differed from the digital original.
- Tables read with OCR have `ocrUsed: true` and a `suspiciousValues` list flagging values that look misread, with row, column and reason. In the same test it flagged 12 of the 13 numeric errors. It is a prompt to check, not a guarantee: values it doesn't flag can still be wrong.
- **Verify important figures against the PDF**, especially for scans, low-resolution images, handwriting, or non-English text. Only English OCR is included.
- Digital PDFs don't need OCR. Leave it off for faster, cheaper and more accurate results.

### Limits and accuracy

- Table detection is machine learning, not magic. It can miss borderless tables, split or merge cells, or misread complex layouts. "No tables found" doesn't prove a PDF has no tables.
- Tables that continue across pages are returned as separate tables, one per page.
- Header rows are detected automatically. Occasionally the first data row is treated as part of the header.
- Contents-page detection is heuristic. Turn on `includeNonDataTables` if a real table goes missing.
- Up to 100 PDFs, 200 pages per PDF and 100 MiB per file per run. Password-protected PDFs, login-only links, private-network addresses and non-standard ports are not supported.
- The default memory is 4 GB. Most PDFs peak at 1–2.2 GB. Choose 8 GB for roughly 1.7× faster processing.

### Your responsibility

Only submit PDFs that you have the right to download and process, and respect any copyright, licence or confidentiality terms that apply to them. Results and downloaded files are stored in your Apify account storage and follow your account's retention settings.

### Attribution and licences

This Actor is an independent product. It is **powered by the open-source Docling library** (MIT licence) and is not affiliated with, endorsed by or sponsored by IBM or the Docling project. It also uses:

- Docling's layout model (Apache-2.0) and TableFormer model weights (CDLA-Permissive-2.0)
- PyTorch (BSD-style licence)
- Tesseract OCR (Apache-2.0)
- the Apify SDK (Apache-2.0)

Licence texts and notices ship with the Actor image (`THIRD_PARTY_NOTICES.md` and the `licenses/` folder).

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct links to public PDF files (HTTP/HTTPS), up to 100 per run. Duplicates are processed once. Only submit PDFs you have the right to process.

## `maxPages` (type: `integer`):

Process the first N pages of each PDF. You are charged per processed page, so keep this as low as you need.

## `enableOcr` (type: `boolean`):

Read scanned pages (no text layer) with English OCR. Scanned pages are charged at the OCR page price. OCR can misread digits and decimal points; check suspiciousValues and verify important numbers.

## `tableMode` (type: `string`):

Accurate is recommended for complex tables.

## `headerStyle` (type: `string`):

How stacked column headers are named. joined: 'Parent / Child'. leaf: only the lowest header. dotted: 'Parent.Child' (Docling's original format).

## `mergedCells` (type: `string`):

first: a merged cell's text appears once, in its first cell (section labels are not repeated across columns). repeat: copy the text into every cell it covers.

## `includeNonDataTables` (type: `boolean`):

By default, tables that look like a table of contents or list of figures (dot leaders, page-number columns) are skipped. Turn on to keep them, marked tableType 'contents'.

## `safeCsv` (type: `boolean`):

Prefix formula-like CSV cells with an apostrophe. JSON retains original text.

## `maxFileSizeMb` (type: `integer`):

Reject larger PDF downloads.

## `documentTimeoutSeconds` (type: `integer`):

Stop a conversion taking longer than this limit. Download time is separate.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://raw.githubusercontent.com/docling-project/docling/main/tests/data/pdf/sources/2206.01062.pdf"
  ],
  "maxPages": 20,
  "enableOcr": false,
  "tableMode": "accurate",
  "headerStyle": "joined",
  "mergedCells": "first",
  "includeNonDataTables": false,
  "safeCsv": true,
  "maxFileSizeMb": 25,
  "documentTimeoutSeconds": 180
}
```

# Actor output Schema

## `results` (type: `string`):

One record per extracted table (columns, rows, pages, CSV/JSON links) and one per PDF (status, pages, warnings, errors).

## `files` (type: `string`):

Individual table-NNN.csv and table-NNN.json files for each PDF.

## `summary` (type: `string`):

Totals for documents, pages, tables, billable events and any URLs skipped because of the max charge limit.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "pdfUrls": [
        "https://raw.githubusercontent.com/docling-project/docling/main/tests/data/pdf/sources/2206.01062.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kiwitools/pdf-tables-to-csv-json").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "pdfUrls": ["https://raw.githubusercontent.com/docling-project/docling/main/tests/data/pdf/sources/2206.01062.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("kiwitools/pdf-tables-to-csv-json").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "pdfUrls": [
    "https://raw.githubusercontent.com/docling-project/docling/main/tests/data/pdf/sources/2206.01062.pdf"
  ]
}' |
apify call kiwitools/pdf-tables-to-csv-json --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kiwitools/pdf-tables-to-csv-json"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/iHbo8Tsr0fA9Y3kaZ/builds/zSwo7f8rZThKjsbh3/openapi.json
