# Evidence-First PDF Table Extractor (`defenestrator/evidence-first-pdf-table-extractor`) Actor

Extract PDF tables to CSV and JSON with page and bounding-box provenance, OCR fallback, and explicit quality flags.

- **URL**: https://apify.com/defenestrator/evidence-first-pdf-table-extractor.md
- **Developed by:** [Defenestrator](https://apify.com/defenestrator) (community)
- **Categories:** Developer tools, AI, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 document processeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Evidence-First PDF Table Extractor

Extract tables from digital and scanned PDFs into full CSV and JSON exports while preserving **where each table came from**.

This Actor extracts PDF tables with review-oriented provenance, bounded outputs, explicit quality flags, and transparent pay-per-event pricing.

### Why use it

Basic PDF text extractors flatten tables and discard evidence. This Actor produces:

- one dataset row per detected table;
- full CSV and JSON files in the default key-value store;
- source URL, final URL, PDF SHA-256, page number, and parser bounding box where available;
- OCR fallback for scanned regions using local CPU models;
- explicit structural quality flags and a documented heuristic score;
- bounded cells/exports plus spreadsheet-formula escaping in CSV;
- an error or no-table row for every document that does not produce tables.

No PDF content is sent to remote document-processing services. Docling and RapidOCR run inside the Actor container.

### Input

Use `pdfSources` to upload PDFs or provide public PDF URLs. Defaults are deliberately bounded:

- 10 documents per run;
- 50 pages per PDF;
- 25 MB per PDF;
- 60-second absolute DNS/download deadline;
- 180-second hard conversion deadline;
- 3,072 MB isolated conversion-process RSS limit;
- 4,096 MB minimum Actor memory, with 768 MB reserved for the parent process;
- standard HTTP/HTTPS ports only;
- every connection pinned to a validated public IP; private, loopback, link-local, and metadata-network destinations blocked;
- HTTPS-to-HTTP redirect downgrades rejected.

`ocrMode` values:

- `auto` — inspect the PDF text layer, skip OCR when every processed page already has useful text, otherwise OCR bitmap regions; OCR-derived tables are beta and always flagged `ocr_extraction_requires_review`;
- `always` — force full-page OCR;
- `never` — disable OCR.

The requested and effective OCR modes are both reported in output rows.

`tableMode: fast` is the cost-controlled default. `accurate` can improve difficult grouped headers and merged cells, but it is materially slower. Neither mode is presented as universally superior; inspect quality flags and provenance for consequential use.

### Output

Missing parser bounding boxes, coordinates, or coordinate origins are represented conservatively and flagged with `missing_bbox_provenance`; page-only or origin-less evidence is not presented as complete bounding-box provenance.

Every dataset row and JSON control record includes `contractVersion: 1`. Every dataset row also includes a one-based `sourceIndex`. Document and table IDs include the input occurrence, so repeated URLs or different URLs serving identical bytes cannot overwrite one another. Persisted `sourceUrl` and `finalUrl` values are intentionally redacted and are not raw retrievable URLs.

Dataset rows use `recordType: table` for extracted tables and `recordType: document` for `no_tables`, `error`, `partial_write`, or `billing_error` outcomes. Every row is validated against a field whitelist and a six-megabyte serialized-size ceiling before persistence.

Full exports are stored under:

```text
table-<document-id>-001.csv
table-<document-id>-001.json
document-<document-id>.md
document-<document-id>-manifest.json
OUTPUT
```

The dataset includes only a bounded preview; use `csvRecordKey` or `jsonRecordKey` for the complete table. The document manifest is written last in the core persistence sequence. `status: complete` marks fully persisted core output; `status: partial_write` explicitly lists artifacts and dataset rows written before a late storage failure; `status: billing_error` means core output was complete but a configured PPE charge did not fully settle. Confirmed partial charges and the failed event are preserved in the finalized manifest and `OUTPUT`. JSON preserves bounded extracted values. CSV prefixes spreadsheet-formula-like strings with an apostrophe; `outputFlags` and `spreadsheetEscapedCells` report when that happened.

### Quality score

`qualityScore` is a transparent structural heuristic, **not model confidence or measured extraction accuracy**. It penalizes empty/single-column tables, duplicate or blank headers, high blank-cell ratios, missing page provenance, and missing/incomplete bounding-box coordinates or coordinate origin. Run summaries therefore use `structurallyUnflaggedTables` and `structurallyFlaggedTables`, not “high quality.” Consumers should validate critical data against the cited PDF page and bounding box.

### Pricing and PPE billing

This Actor uses **pay per event + Apify platform usage**. It preflights the user's maximum run budget before conversion and charges custom events only after document artifacts, dataset rows, and the core completion manifest are persisted.

Configured events:

- `apify-actor-start` — `$0.00005` per allocated memory GB, charged automatically by Apify;
- `document-processed` — `$0.010` per successfully persisted document;
- `digital-page` — `$0.002` per selected page when OCR is skipped;
- `ocr-page` — `$0.010` per selected page when OCR runs.

The synthetic `apify-default-dataset-item` event is deliberately disabled to avoid duplicate dataset-output charges.

Failed downloads, safety rejections, conversion failures, and OCR page-limit rejections are not charged by these custom events. Because platform usage is passed through, users can still incur small Apify platform-usage charges for failed runs even when no custom event is charged.

### Limitations

- OCR is capped at three selected pages per document in this release. Set `maxPagesPerDocument` to `3` or less, split longer scans, or use `ocrMode: never` for digital PDFs.
- OCR table extraction is beta. Every OCR-derived table carries `ocr_extraction_requires_review`; clear image tables can still be missed, and detected cells can be misaligned.
- Complex nested headers, rotated tables, handwriting, and low-resolution scans may require review.
- OCR has been validated on English synthetic and public samples; broader language coverage is not established.
- Bounding-box coordinates use the parser's reported coordinate origin.
- URL DNS resolution has an absolute deadline; each connection uses the validated public IP directly while TLS verifies the original hostname. Redirects repeat validation and pinning.
- PDFium preflight and Docling conversion run in separate spawned process groups. The parent enforces hard wall-clock deadlines, samples process-tree RSS every 250 ms, and kills a group on an observed breach. RSS supervision is not a kernel/cgroup hard cap; a brief spike or container OOM can still precede the next sample.
- After Actor storage initialization, Python-level input, document, and fail-fast errors persist `OUTPUT` through an outer `finally`; Actor initialization failure, infrastructure termination, or container OOM can still prevent an application-level finalizer.
- Tables, provenance, previews, dataset rows, and exports are bounded; documents exceeding those bounds fail explicitly rather than silently truncating whole tables.
- The Actor extracts tables; it does not provide legal, financial, medical, or regulatory interpretation.
- This Actor is CPU-oriented and prioritizes inspectable output over speed.

### Example input

```json
{
  "pdfSources": ["https://arxiv.org/pdf/2206.01062"],
  "ocrMode": "auto",
  "tableMode": "fast",
  "maxDocuments": 1,
  "maxPagesPerDocument": 20,
  "maxFileSizeMb": 10,
  "downloadTimeoutSeconds": 60,
  "documentTimeoutSeconds": 180,
  "conversionMemoryLimitMb": 3072,
  "saveDocumentMarkdown": true,
  "failRunOnDocumentError": false
}
```

# Actor input Schema

## `pdfSources` (type: `array`):

Upload PDF files or provide public HTTP/HTTPS PDF URLs. Private, loopback, link-local, and cloud-metadata destinations are blocked.

## `ocrMode` (type: `string`):

Auto OCRs bitmap regions while retaining embedded PDF text. Always forces full-page OCR. Never disables OCR. OCR table extraction is beta and every OCR-derived table is flagged for review. Effective OCR is limited to three selected pages per document; split longer scans or set the page limit to 3 or less.

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

Fast is the cost-controlled default. Accurate can improve complex/grouped headers but takes materially longer.

## `maxDocuments` (type: `integer`):

Hard run cap. Extra supplied documents are not processed.

## `maxPagesPerDocument` (type: `integer`):

Processing stops at this page limit for each PDF. If OCR is effective, this value must be 3 or less; digital PDFs with OCR skipped can use the full range.

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

Download and conversion safety limit per PDF.

## `downloadTimeoutSeconds` (type: `integer`):

Absolute DNS, connection, redirect, and body download deadline per PDF.

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

Hard wall-clock deadline for the isolated Docling process. PDF preflight is separately isolated and capped at the lesser of 30 seconds or this value.

## `conversionMemoryLimitMb` (type: `integer`):

PDF preflight and Docling process-tree RSS are sampled every 250 ms and the process group is killed on an observed breach. This is not a kernel/cgroup hard cap. The value must fit within allocated Actor memory while leaving a 768 MB parent-process reserve.

## `saveDocumentMarkdown` (type: `boolean`):

Save a bounded full-document Markdown export in the default key-value store.

## `failRunOnDocumentError` (type: `boolean`):

Normally each failed PDF produces an explicit error row and the remaining PDFs continue.

## Actor input object example

```json
{
  "pdfSources": [
    "https://arxiv.org/pdf/2206.01062"
  ],
  "ocrMode": "auto",
  "tableMode": "fast",
  "maxDocuments": 10,
  "maxPagesPerDocument": 50,
  "maxFileSizeMb": 25,
  "downloadTimeoutSeconds": 60,
  "documentTimeoutSeconds": 180,
  "conversionMemoryLimitMb": 3072,
  "saveDocumentMarkdown": true,
  "failRunOnDocumentError": false
}
```

# Actor output Schema

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

No description

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

No description

## `exports` (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 = {
    "pdfSources": [
        "https://arxiv.org/pdf/2206.01062"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("defenestrator/evidence-first-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 = { "pdfSources": ["https://arxiv.org/pdf/2206.01062"] }

# Run the Actor and wait for it to finish
run = client.actor("defenestrator/evidence-first-pdf-table-extractor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "pdfSources": [
    "https://arxiv.org/pdf/2206.01062"
  ]
}' |
apify call defenestrator/evidence-first-pdf-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=defenestrator/evidence-first-pdf-table-extractor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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