# Checked PDF Table Extractor (`jyinnovation/checked-pdf-table-extractor`) Actor

PDFs and scans to CSV/Excel tables plus a validation report that flags wrong totals, broken computed columns, unparsed numbers and low-confidence OCR, so you know which numbers to trust.

- **URL**: https://apify.com/jyinnovation/checked-pdf-table-extractor.md
- **Developed by:** [JYInnovation](https://apify.com/jyinnovation) (community)
- **Security:** Limited permissions
- **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

## Checked PDF Table Extractor: PDF and Scan to CSV, Excel and JSON with Validation

**Turn PDFs and scanned documents into clean CSV, Excel or JSON tables, plus a validation report that tells you which numbers to trust.** Every table is checked: totals are re-added, computed columns are re-computed, numbers that failed to parse are flagged cell by cell, and low-confidence OCR is marked. You get a `PASS` / `WARN` / `FAIL` per table and a human-readable `REPORT.html`.

### Who it is for

- **Finance and bookkeeping teams and developers** turning invoices, bank and account statements and financial reports into spreadsheets or API data without silently importing a wrong total.
- **Analysts and researchers** pulling price lists, government and statistical tables out of PDFs.
- **Automation builders** who need a machine-readable pass/fail signal (`checks.status`) to route documents to review.

Works on invoices, statements, reports, price lists and government tables, as text PDFs or scans (PNG, JPG, TIFF).

### Why the validation report matters

Extraction errors are silent. A misread digit in a scanned invoice, a dropped row at a page break or a total that never matched the line items all look like perfectly normal data in a spreadsheet. This Actor does the cross-checks a careful bookkeeper would, automatically, on every table:

| Check | What it catches | Result |
|---|---|---|
| **Totals cross-foot** | Rows labelled *Total, Subtotal, Sum, Grand total* (also *Gesamt, Summe, Zwischensumme, Totale, Total general, Sous-total ...*) are compared with the sum of the rows above, including Subtotal + Tax + Shipping = Total layouts. | `FAIL` with expected vs found and the cell address |
| **Computed columns** | If at least 80% of rows satisfy `qty x unit price = amount`, `a + b = c`, `a - b = c`, or `Q1 + Q2 + Q3 + Q4 = Total`, the rows that do not are flagged. | `FAIL` with expected vs found per row |
| **Numeric parse rate** | Cells that look like numbers but cannot be parsed (`1,23,4.5`, `1O0.00` with a letter O, mixed separators). Reported per column with exact cell coordinates. | `WARN` |
| **OCR confidence** | For OCR'd pages every word keeps its Tesseract confidence; cells below 60 are flagged. | `WARN` |
| **Structure** | Inconsistent column counts, empty or merged cells, empty rows and columns, header rows repeated mid-table (removed and reported). | `WARN` / info |
| **Page coverage** | Pages where no table was found, pages skipped, pages not processed because of a limit. | listed per file |

A difference of a few units in the last digit (for example 0.01 on a 2-decimal invoice) is reported as a rounding warning instead of a failure.

**PASS means every check that could run agreed. It does not prove the extraction is perfect** - for example, a table with no totals and no computed columns can only be checked for number parsing. The report says exactly what was checked (`totals_checked`, `computed_columns`).

### What you get

- **Dataset**: one item per extracted table - `file`, `table_index`, `pages`, `n_rows`, `n_cols`, `header`, `rows` (numbers are real numbers; capped at 500 rows with a `rows_truncated` flag), links to the full files, and a `checks` summary with the status and every issue.
- **Key-value store** (default store of the run):
  - `<n>_<file>_table<k>.csv`, `.xlsx`, `.json` for every table (the XLSX highlights flagged cells and has a *Checks* sheet)
  - `REPORT.html` - open it in a browser: one section per table, issues highlighted, data preview
  - `REPORT.json` - the same report, machine-readable
  - `OUTPUT` - run summary (files, tables, PASS/WARN/FAIL counts, pages processed, report links)
- Files that fail (password-protected PDF, broken file, HTML page instead of a PDF, 404, over 50 MB) produce a clear **error item** and never stop the other files.

### How to use

1. Paste PDF or image URLs (PDF, PNG, JPG, TIFF) into **PDF / image URLs**, or upload one file.
2. Leave **OCR** on *Auto*: pages with a text layer are read directly, scanned pages are OCR'd.
3. Run it, then open `REPORT.html` in the key-value store, or fetch the dataset through the API.

#### Input

```json
{
  "files": ["https://example.com/invoice-2024-0042.pdf", "https://example.com/scanned-statement.pdf"],
  "pages": "1-3,5",
  "ocr": "auto",
  "ocrLanguage": "eng",
  "mergeMultiPageTables": true,
  "outputFormats": ["csv", "xlsx"],
  "numberLocale": "auto",
  "maxPages": 200
}
```

| Field | Meaning |
|---|---|
| `files` | URLs of PDFs / images. `fileUpload` lets you upload one file instead. |
| `pages` | Optional page selection such as `1-3,5` or `4-`. Applies to every file. |
| `ocr` | `auto` (OCR only pages with no text layer), `always`, `never`. |
| `ocrLanguage` | Installed: `eng`, `deu`, `fra`, `spa`. Combine with `+`, e.g. `eng+deu`. |
| `mergeMultiPageTables` | Join a table continued on the next page (repeated header, or same column count and data-like first row). |
| `outputFormats` | Any of `csv`, `xlsx`, `json`. |
| `numberLocale` | `auto`, `us` (1,234.56) or `eu` (1.234,56). Negatives such as `(1,234.00)` and `45-`, currency signs and `%` are understood. Percentages keep their face value (`12%` becomes `12`). |
| `maxPages` | Safety cap on processed (billed) pages per run, default 200. |

#### Output example (dataset item, shortened)

```json
{
  "item_type": "table",
  "file": "invoice-2024-0042.pdf",
  "table_index": 1,
  "pages": [1],
  "extraction_method": "lines",
  "n_rows": 8,
  "n_cols": 4,
  "header": ["Item", "Qty", "Unit Price", "Amount"],
  "rows": [["Widget A", 4, 12.5, 50], ["Widget B", 10, 3.2, 32]],
  "rows_truncated": false,
  "csv_url": "https://api.apify.com/v2/key-value-stores/.../records/01_invoice-2024-0042_table1.csv",
  "xlsx_url": "https://api.apify.com/v2/key-value-stores/.../records/01_invoice-2024-0042_table1.xlsx",
  "checks": {
    "status": "FAIL",
    "issue_counts": { "FAIL": 1, "WARN": 0, "INFO": 0 },
    "issues": [{
      "code": "TOTAL_MISMATCH",
      "severity": "FAIL",
      "message": "'Total' in column 'Amount' is 632.14 but the sum of the subtotals plus the rows after the last one is 623.14 (difference 9.00).",
      "cell": "D9", "page": 1, "expected": "623.14", "found": "632.14", "difference": "9.00"
    }],
    "totals_checked": 2,
    "totals_ok": 1,
    "computed_columns": [{ "formula": "Qty x Unit Price = Amount", "rows_checked": 5, "rows_ok": 5, "match_rate": 1.0 }]
  }
}
```

Cell addresses (`D9`) refer to the exported CSV/XLSX, where row 1 is the header.

### Pricing

Pay per event. You are charged for **pages processed**, exactly one event per page:

| Event | When | Price |
|---|---|---|
| `page-processed` | a page with a text layer (read directly) | **$0.01 / page** |
| `ocr-page` | a page that had to be OCR'd (scans, photos and images; each image or TIFF frame counts as one page) | **$0.02 / page** |

Examples: a 20-page text report is about $0.20; 100 scanned pages are about $2.00. Tables, files, reports and validation are included - there is no per-table or per-check fee. A page that was processed is charged even if no table is found on it (use the **Pages** field to process only the pages you need). Pages that fail to process, are skipped (for example OCR set to `never`) or are never started are not charged. Set a **maximum charge per run** in the run options to cap your spend: when it is reached the Actor stops starting new pages, writes everything finished so far, lists the unprocessed pages in the report and says so in the status message.

### Limitations

- **Handwriting** is not supported; neither are very low-quality, skewed or rotated scans. Scan at 200 dpi or more, straight and upright, for good OCR.
- **Complex tables**: deeply nested headers, heavily merged cells, tables with row spans or multi-line headers, and several tables side by side can come out with shifted columns. Merged and missing cells are reported. Newspaper-style multi-column text is not treated as a table.
- **Borderless tables** are reconstructed from word positions. Very tight column spacing can merge columns - check the preview in `REPORT.html`.
- Tables embedded as pictures inside a text PDF are not OCR'd unless you set `ocr` to `always`.
- OCR languages installed: English, German, French, Spanish. No Asian-script or right-to-left support.
- Validation needs something to validate against: totals need a row labelled Total/Subtotal/Sum (or equivalent), computed columns need at least 5 data rows and 80% agreement. A table without either is checked for number parsing and structure only.
- Password-protected PDFs are rejected with a clear error. Limits: 50 MB per file, 200 pages per run by default.
- Totals that include unparsed numbers are reported as *unverifiable* (`WARN`), never silently passed.

### Privacy and data handling

Use it for **public documents only**. Each file is downloaded into the run's memory, processed transiently and not retained as a source copy; OCR runs inside the Actor and no content is sent to any third-party service. Extracted tables, reports and (if you used the upload field) the uploaded file live in your run's own dataset and key-value store under your Apify retention settings; delete the run to remove them. Links to private networks and `localhost` are refused.

### Integrations

Use the Apify API, Make, Zapier, Google Sheets or webhooks. Typical pattern: run on a schedule or on new files, read the dataset, and alert (Slack/email) whenever `checks.status` is `FAIL` or `WARN`.

```python
from apify_client import ApifyClient
run = ApifyClient("<APIFY_TOKEN>").actor("<owner>/checked-pdf-table-extractor").call(
    run_input={"files": ["https://example.com/invoice.pdf"]})
for t in ApifyClient("<APIFY_TOKEN>").dataset(run["defaultDatasetId"]).iterate_items():
    print(t["file"], t["checks"]["status"], t["checks"]["issues"])
```

### FAQ

**Does PASS mean the extraction is perfect?**
No. PASS means every check that could run agreed. A table with no total row and no computed columns can only be checked for number parsing and structure, and the report lists exactly what was checked.

**Can it read scanned documents?**
Yes. With **OCR** on *Auto*, pages without a text layer are OCR'd (English, German, French, Spanish). Handwriting is not supported.

**What do I get for a failed file?**
A clear error item in the dataset (password-protected, broken, not a PDF, 404, over 50 MB). Other files in the run are not affected.

**European number formats?**
Yes. Use **Number format** `eu` for 1.234,56, or leave it on `auto`.

**Where are the CSV and Excel files?**
In the run's key-value store, one file per table per selected format, with links in the dataset items and in `REPORT.html`.

### Support

Found a document that extracts badly? Open an issue on the Actor page with the (public) link and what you expected. JYInnovation reads every report.

Built by **JYInnovation**.

# Actor input Schema

## `files` (type: `array`):

Links to PDF, PNG, JPG or TIFF files (public documents). Add one URL per line. You can also use the upload field below.

## `fileUpload` (type: `string`):

Upload one PDF or image instead of (or in addition to) URLs.

## `pages` (type: `string`):

Which pages to process, e.g. <code>1-3,5</code> or <code>4-</code>. Empty = all pages. Applies to every file.

## `ocr` (type: `string`):

<b>auto</b>: OCR only pages that have no text layer (scans). <b>always</b>: OCR every page (useful when the text layer is garbled). <b>never</b>: skip pages without a text layer. OCR pages cost more than text pages.

## `ocrLanguage` (type: `string`):

Tesseract language code. Installed: <code>eng</code>, <code>deu</code>, <code>fra</code>, <code>spa</code>. Combine with +, e.g. <code>eng+deu</code>.

## `mergeMultiPageTables` (type: `boolean`):

Join a table that continues on the next page when its header repeats or the columns line up. Repeated header rows are removed and reported.

## `outputFormats` (type: `array`):

Files saved per table in the run's key-value store. The dataset always contains the table rows and checks; REPORT.html and REPORT.json are always created.

## `numberLocale` (type: `string`):

<b>auto</b> decides per table. <b>us</b> = 1,234.56. <b>eu</b> = 1.234,56. Negatives like (1,234.00) and 45- are understood; currency signs and % are removed.

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

Safety cap on the number of pages processed (and billed) in one run, across all files.

## Actor input object example

```json
{
  "files": [
    "https://raw.githubusercontent.com/camelot-dev/camelot/master/docs/_static/pdf/foo.pdf"
  ],
  "ocr": "auto",
  "ocrLanguage": "eng",
  "mergeMultiPageTables": true,
  "outputFormats": [
    "csv",
    "xlsx"
  ],
  "numberLocale": "auto",
  "maxPages": 200
}
```

# Actor output Schema

## `tables` (type: `string`):

No description

## `report` (type: `string`):

No description

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

No description

# 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 = {
    "files": [
        "https://raw.githubusercontent.com/camelot-dev/camelot/master/docs/_static/pdf/foo.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jyinnovation/checked-pdf-table-extractor").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 = { "files": ["https://raw.githubusercontent.com/camelot-dev/camelot/master/docs/_static/pdf/foo.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("jyinnovation/checked-pdf-table-extractor").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 '{
  "files": [
    "https://raw.githubusercontent.com/camelot-dev/camelot/master/docs/_static/pdf/foo.pdf"
  ]
}' |
apify call jyinnovation/checked-pdf-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jyinnovation/checked-pdf-table-extractor"
        }
    }
}
```

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/dr3AViBNKnMNlyGsJ/builds/fxQfFeXwAeelVtflV/openapi.json
