# Invoice, Receipt & PDF Table to JSON/Markdown OCR (`panix/invoice-table-json-extractor`) Actor

Extracts structured JSON tables and clean LLM Markdown from PDF invoices, bank statements, and financial reports using IBM Docling.

- **URL**: https://apify.com/panix/invoice-table-json-extractor.md
- **Developed by:** [Panix AI LAB](https://apify.com/panix) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$15.00 / 1,000 pdf document parseds

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

## Invoice, Receipt & PDF Table to JSON / Markdown OCR

Turn PDF invoices, receipts, bank statements and scanned documents into **structured JSON tables** and **clean, LLM-ready Markdown**. The Actor reads born-digital PDFs directly from their text layer and runs **OCR only on pages that need it**: scanned pages, photos and image files (JPG, PNG, TIFF, BMP, WEBP). Table structure is reconstructed with IBM **Docling** (layout analysis + TableFormer), and OCR uses RapidOCR (PP-OCRv6). All models are bundled in the Actor, so you need no external API keys and no document leaves the run for a third-party AI service.

### What does it do?

For each document URL you provide, the Actor:

1. Downloads the file over a validated http(s) connection and checks that it really is a PDF or an image.
2. Looks at every PDF page: does it have a real embedded text layer, or is it a scan?
3. Parses digital pages without OCR (fast and cheap) and runs full-page OCR on scanned pages and images.
4. Detects tables, including borderless ones, and exports every table as JSON (`headers` plus row objects).
5. Exports the whole document as Markdown that keeps headings, reading order and tables.
6. Saves one result per document to the dataset, with page counts, warnings and a clear status.

### Use cases

- **Invoice OCR and invoice data extraction:** line items, totals, invoice numbers and due dates to JSON for accounting, ERP or Google Sheets.
- **Receipt OCR:** photos or scans of receipts to text and tables for expense tools.
- **Bank statement parser:** transaction tables from PDF statements to JSON or CSV-ready rows.
- **Financial reports and price lists:** multi-page PDF table extraction.
- **Scanned PDF to Markdown or text:** archives, contracts and letters without a text layer.
- **RAG and AI agents:** Markdown for vector databases (Pinecone, Qdrant, Supabase) and MCP agents.
- **No-code automation:** call it from n8n, Make or Zapier and map `tables_json` into your workflow.

### Supported inputs

- **PDF** (born-digital, scanned, or mixed), and **images**: JPG, PNG, TIFF (multi-page included), BMP, WEBP.
- Each URL must be a **direct, public download link** that returns the file itself (not an HTML preview page). Signed links such as S3 pre-signed URLs work.
- Only `http://` and `https://` links to public hosts are accepted. Local files, private network addresses and links with embedded credentials are rejected.
- Password-protected PDFs are not supported; they come back as `failed` with a clear message.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `documentUrls` | array of strings | required | Direct links to PDFs or images. Duplicates are removed. Objects like `{"url": "..."}` are accepted too. |
| `ocrMode` | `auto` / `always` / `never` | `auto` | How OCR is used, see below. |
| `extractTablesAsJson` | boolean | `true` | Export tables as JSON in `tables_json`. Tables are always included in the Markdown. |
| `maxPages` | integer (1-2000) | `100` | Parse at most this many pages per document, starting from page 1. |
| `maxFileSizeMb` | integer (1-200) | `50` | Larger files are rejected before parsing. |
| `maxDocuments` | integer (1-1000) | `100` | Process at most this many URLs per run; extra URLs are ignored with a warning. |
| `documentTimeoutSecs` | integer (30-3600) | `300` | Time limit per document; pages finished so far are returned as a partial result. |

Example input:

```json
{
  "documentUrls": [
    "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"
  ],
  "ocrMode": "auto",
  "extractTablesAsJson": true,
  "maxPages": 100,
  "maxFileSizeMb": 50,
  "maxDocuments": 100,
  "documentTimeoutSecs": 300
}
```

### OCR modes

- **`auto` (recommended):** every PDF page is checked for an embedded text layer. Pages with enough real text are parsed directly, **without OCR**, and images or logos inside them are never OCR'd. Pages with little or no text (scans, photos, image-only pages) get full-page OCR. In a **mixed PDF**, only the scanned pages are OCR'd: the document is processed in runs of consecutive pages of the same kind and the results are merged back in page order. Pages that already carry an invisible OCR text layer count as digital. Image files always get OCR.
- **`always`:** full-page OCR on every page, even if a text layer exists. Use it when the embedded text is broken or garbled.
- **`never`:** no OCR at all. Scanned pages come back empty (with a warning) and image files fail with a message asking for `auto` or `always`.

### Output

One dataset item per document. You can download the dataset as JSON, CSV, Excel or XML, or open the **Overview**, **Tables** and **Markdown** views in the Output tab.

| Field | Description |
| --- | --- |
| `source_url` | The URL from your input. |
| `file_name` | File name taken from the URL. |
| `document_type` | `pdf` or `image` (null if the file could not be downloaded or recognised). |
| `status` | `success`, `partial_success` (some pages missing, see `warnings`), `failed` or `skipped` (see `error_message`). |
| `error_message` | Why the document failed or was skipped; null on success. |
| `warnings` | Non-fatal notes, for example pages cut by `maxPages` or a timeout. |
| `ocr_mode` | The OCR mode used. |
| `pages_total` | Pages (or image frames) in the file. |
| `pages_processed` | Pages parsed in this run. |
| `pages_digital` | Pages parsed from the text layer, without OCR. |
| `pages_ocr` | Pages parsed with OCR. |
| `table_count` | Number of tables in `tables_json`. |
| `tables_json` | Tables with `table_index`, `page_number`, `headers`, `rows`, `row_count`, `column_count`. |
| `full_markdown` | Markdown of the processed pages. |
| `markdown_url` | Set only for very large documents: link to the Markdown file in the key-value store (then `full_markdown` is null). |
| `tables_url` | Set only when the tables alone are too large for a dataset item: link to the tables JSON (then `tables_json` is empty). |
| `markdown_length` | Characters in the Markdown. |
| `processing_time_secs` | Seconds spent on this document. |
| `processed_at` | UTC timestamp. |

Headers are always unique (duplicates become `Amount`, `Amount_2`; empty ones become `column_3`) so no cell is lost, and empty cells are `null`. Tables without a header row use `0`, `1`, `2` as column names.

Sample item (shortened):

```json
{
  "source_url": "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf",
  "file_name": "wordpress-pdf-invoice-plugin-sample.pdf",
  "document_type": "pdf",
  "status": "success",
  "error_message": null,
  "warnings": [],
  "ocr_mode": "auto",
  "pages_total": 1,
  "pages_processed": 1,
  "pages_digital": 1,
  "pages_ocr": 0,
  "table_count": 3,
  "tables_json": [
    {
      "table_index": 1,
      "page_number": 1,
      "headers": ["Hrs/Qty", "Service", "Rate/Price", "Adjust", "Sub Total"],
      "rows": [
        {
          "Hrs/Qty": "1.00",
          "Service": "Web Design This is a sample description...",
          "Rate/Price": "$85.00",
          "Adjust": "0.00%",
          "Sub Total": "$85.00"
        }
      ],
      "row_count": 1,
      "column_count": 5
    }
  ],
  "full_markdown": "## From:\nDEMO - Sliced Invoices ...\n\n| Hrs/Qty | Service | Rate/Price | Adjust | Sub Total |\n|---|---|---|---|---|\n| 1.00 | Web Design ... | $85.00 | 0.00% | $85.00 |\n...",
  "markdown_url": null,
  "tables_url": null,
  "markdown_length": 909,
  "processing_time_secs": 6.1,
  "processed_at": "2026-10-20T08:00:00.000000+00:00"
}
```

A failed document looks like this, and never stops the rest of the run:

```json
{
  "source_url": "https://example.com/invoice",
  "status": "failed",
  "error_message": "The URL returned an HTML page, not a file. Use a direct download link to the PDF or image.",
  "pages_processed": 0
}
```

### Pricing

The Actor uses **pay-per-event pricing, charged per page**, so you pay for what was actually parsed:

- **`page-parsed`:** each page read from its embedded text layer (digital PDF pages).
- **`page-ocr`:** each page that needed OCR (scanned pages, photos, image files). OCR takes much more compute, so this event costs more than a digital page.

The current prices are shown on the Actor's **Pricing** tab. What you are not charged for:

- Documents that fail or are skipped.
- Pages beyond `maxPages`, or pages that were not processed because of a timeout.
- Failed page ranges inside a partial result.

The Actor respects your **maximum charge per run**. Before each document it checks how many pages still fit in your limit, parses only those, and marks the rest as `skipped` or `partial_success`, so a run never goes over the amount you set.

### Limits and performance

- Default memory is 4 GB, which is enough for long documents because pages are converted in chunks of 10. More memory also gives more CPU, which speeds up OCR.
- Digital pages take a few seconds each; OCR pages take longer. The first page of a run also loads the models.
- The Actor watches the run timeout. When it gets close, it stops starting new work and saves what it has, so the run still finishes successfully. Documents that were not started are listed as `skipped`. For big batches, raise the run timeout or split the input.
- OCR uses the multilingual PP-OCRv6 model (best for Latin-script and Chinese text). Handwriting and very low-resolution photos are recognised less reliably.
- A dataset item can hold about 9 MB. For bigger documents the Markdown (and if needed the tables) is saved to the run's key-value store and linked from `markdown_url` / `tables_url`.

### Use it via API or integrations

Run the Actor and get the results in one call (replace `<YOUR_API_TOKEN>`):

```bash
curl -X POST "https://api.apify.com/v2/acts/panix~invoice-table-json-extractor/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"], "ocrMode": "auto"}'
```

Python with the `apify-client` package:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("panix/invoice-table-json-extractor").call(
    run_input={"documentUrls": ["https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"]}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["status"], item["table_count"], item["pages_ocr"])
```

It also works with the Apify integrations for **n8n, Make, Zapier, Google Drive, Slack and webhooks**, and as a tool for AI agents through the Apify MCP server.

### Data and privacy

- Documents are downloaded only from the URLs you provide and processed inside your Actor run. All AI models run locally in the container; nothing is sent to an external AI or OCR service.
- The extracted content (Markdown and tables) is stored in your run's dataset and key-value store and follows your Apify data retention settings. Invoices and statements often contain personal data such as names, addresses and account numbers; you are responsible for having the right to process them.
- Logs show only the host and path of each URL, never query strings, so signed-URL tokens stay out of the log.

### FAQ

**Does it work with scanned PDFs?**
Yes. In `auto` mode, pages without a text layer are OCR'd automatically, and in mixed PDFs only those pages are OCR'd and billed as `page-ocr`.

**Why does an OCR page cost more than a digital page?**
OCR runs a text detection and recognition model on the full page image, which takes several times more compute than reading the existing text layer.

**My document came back as `partial_success`. Why?**
Some pages were not processed: the per-document timeout, the run timeout or your charge limit was reached, or Docling could not read some pages. The `warnings` field says which. You are only charged for the pages that were returned.

**What about password-protected PDFs?**
They are not supported; the item is `failed` with a message asking you to remove the password.

**Which languages does OCR support?**
The PP-OCRv6 model handles Latin-script languages (English, German, French, Spanish and more) and Chinese well. Other scripts may give weaker results.

**Can I send Google Drive or Dropbox links?**
Only if the link downloads the file directly. Preview pages return HTML and fail with a clear message.

**How do I process hundreds of documents?**
Pass them all in `documentUrls` (up to `maxDocuments`) and give the run a longer timeout. Each document is independent: one broken file does not affect the others.

### Support

Found a document that does not parse well, or need another feature? Open an issue in the Actor's **Issues** tab and include the URL (or a similar sample) and the run ID.

# Actor input Schema

## `documentUrls` (type: `array`):

Direct public links to the files to parse: PDF (digital or scanned) or images (JPG, PNG, TIFF, BMP, WEBP). Each link must return the file itself, not an HTML page. Only http and https links to public hosts are accepted.

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

auto: detect per page. Pages with a real text layer are parsed directly; pages without one (scans, photos) get OCR. Images always get OCR. always: run full-page OCR on every page. never: no OCR (images fail, scanned pages come back empty). OCR pages are billed as the page-ocr event.

## `extractTablesAsJson` (type: `boolean`):

Export every detected table as JSON (headers plus row objects) in tables_json, next to the full Markdown.

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

Parse at most this many pages of each document, starting from page 1. Remaining pages are skipped and not charged.

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

Files larger than this are rejected before parsing and not charged.

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

Process at most this many document URLs in one run (after removing duplicates). Extra URLs are ignored with a warning.

## `documentTimeoutSecs` (type: `integer`):

Stop parsing a document after this many seconds and return the pages finished so far as a partial result.

## Actor input object example

```json
{
  "documentUrls": [
    "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"
  ],
  "ocrMode": "auto",
  "extractTablesAsJson": true,
  "maxPages": 100,
  "maxFileSizeMb": 50,
  "maxDocuments": 100,
  "documentTimeoutSecs": 300
}
```

# Actor output Schema

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

One item per document with tables_json, full_markdown, page counts and status.

## `overview` (type: `string`):

Status, page counts and table counts per document.

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

Markdown stored in the key-value store when a document is too large for a dataset item.

## `largeTables` (type: `string`):

Tables JSON stored in the key-value store when the tables are too large for a dataset item.

# 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 = {
    "documentUrls": [
        "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("panix/invoice-table-json-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 = { "documentUrls": ["https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("panix/invoice-table-json-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 '{
  "documentUrls": [
    "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf"
  ]
}' |
apify call panix/invoice-table-json-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,panix/invoice-table-json-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/jaQMQ5UkyB5wNpwtT/builds/ZfqlmrZKZeL2Q8itM/openapi.json
