# Invoice Data Extractor: PDF & Image to JSON (No LLM) (`steady_kahikatea/document-data-extractor`) Actor

Extract vendor, invoice number, dates, line items, tax, total and currency from invoice PDFs and clean images as JSON. Rule-based with local OCR: no LLM, no API keys, documents never leave the run. Rejects non-invoices and unreadable photos free.

- **URL**: https://apify.com/steady_kahikatea/document-data-extractor.md
- **Developed by:** [Nick Hellmich](https://apify.com/steady_kahikatea) (community)
- **Categories:** Automation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Data Extractor: PDF & Image to JSON (rule-based, no LLM)

Turn **invoice PDFs and clean invoice images** into structured JSON: vendor, invoice number, dates, line items, subtotal, tax, total, currency, IBAN, with a confidence level on every field and a check that the numbers add up. Extraction is **rule-based with local OCR (Tesseract)**: no LLM, no outside API calls, no API keys, and **your documents never leave the run**. Results are deterministic (same file, same output) and cost a fraction of a cent of compute per digital invoice.

It also reads receipts, forms, tables and custom fields, but **digital invoices are the supported core**. Phone photos of receipts and handwritten invoices are best-effort and clearly weaker (numbers below). If you need near-perfect reading of photos or arbitrary layouts, use a vision-LLM service instead.

### Who it is for

- Accounts-payable and bookkeeping pipelines that receive invoice PDFs by email or upload and need structured rows with a "needs review" flag.
- Developers who want a predictable, cheap, self-contained extractor (no LLM bill, no data sent to third parties) and who will route low-confidence documents to a human.

### What is supported, and how well

| Input | Status | Measured accuracy (see below) |
|---|---|---|
| **Digital invoice PDFs** (text layer; German, English, French, Spanish, Dutch, Italian labels; US and European number/date formats) | **Supported core** | about 90% of checked fields correct, about 5% wrong, about 5% null (blind set #5) |
| **Clean scans and invoice images** (screenshots, flatbed scans) | Supported | about 85-93% correct on blind set #5; lower on poor scans |
| **Phone photos of invoices** | Best-effort | 85% on set #5 (small, mostly well-lit samples); 61-80% on other sets |
| **Receipt photos** (Malaysian, Indonesian, Portuguese, others) | **Weak** | 27% to 66% correct depending on country and photo quality |
| **Handwritten invoices** | **Not reliable** | 44% on set #5 (12-41% on other sets) |
| Non-invoices (letters, contracts, menus, press releases, random text) | Rejected, not charged | 9 of 9 negatives rejected on set #5 |

### Measured accuracy (blind tests, honest numbers)

Accuracy is measured as the share of checked fields (vendor, invoice number, invoice date, due date, subtotal, tax total, total, currency, line-item count) that exactly match hand-checked ground truth. "Wrong" means a value was returned and it was incorrect; "null" means the field was left empty rather than guessed. Test documents are public documents (public datasets, Wikimedia Commons, public sample invoices); the sets are frozen with SHA-256 hashes before scoring.

**Blind set #5 (59 documents, scored once per configuration; 345 checks): 82.0% correct overall (283/345), 5.8% wrong (20), 9.3% null (32), 2.9% rejected as unreadable or not an invoice (10).** Real invoices wrongly rejected as "not an invoice": 1 of 50.

| Category (set #5) | Correct | Notes |
|---|---|---|
| Digital PDFs, all kinds (20 docs, 171 checks) | **90.6%** (155) | 4.7% wrong, 4.7% null |
| - clean invoices | 92% | |
| - credit notes | 100% (8 checks) | |
| - mixed currencies | 93% | |
| - freight / multi-page / hard layouts | 94-100% | |
| - e-invoice sample PDFs (ZUGFeRD-style) | 81% | credit notes and some tax blocks missed |
| Scans (flatbed) | 93% | |
| Invoice images (screenshots) | 86% | |
| Photos of invoices | 85% | |
| Image or scan documents overall (30 docs, 174 checks) | 73.6% | 6.9% wrong, 13.8% null, 5.7% rejected |
| Portuguese phone receipts | 33% | |
| Malaysian receipt photos | 66% | |
| Indonesian receipt photos | 27% | |
| Handwritten | 44% | |

| Field (set #5, all documents) | Correct |
|---|---|
| currency | 100% |
| invoice date | 88% |
| vendor | 82% (30 of 45 exact after normalising) |
| line-item count | 82% |
| due date | 86% |
| subtotal | 80% |
| invoice number | 77% |
| total | 75% |
| tax total | 70% |

Digital PDFs only (set #5): currency 100%, due date 100%, total 95%, invoice date 95%, line-item count 95%, vendor 90%, subtotal 85%, invoice number 85%, tax total 75%.

**Caveats you should know before relying on these numbers**

- Set #5 was frozen before scoring and **not tuned on**. Sets #2, #3 and #4 were also blind when first scored, but were then used for development, so their numbers are optimistic and are not quoted as headline results. Rule changes in this and earlier versions were tuned on development sets.
- The digital PDFs in set #5 come from public invoice generators and sample sets whose template families overlap with ones used during development. **Expect lower accuracy on your own unusual layouts**; on earlier blind sets, clean digital invoices scored 92-96%, mixed-currency invoices 80-93%, and multi-page invoices ranged from 56% to 100%.
- Small samples: set #5 has 59 documents. A single document moves a category by several points.
- Photos and scans: OCR errors are the main failure. The Actor leaves a field empty or lowers its confidence rather than returning an unconfirmed OCR total, but some wrong values still get through (7% of checks on image documents).
- Tax total and invoice number are the weakest fields on every set. Always use the `confidence` and `warnings` fields and verify before posting to your books.
- A document is charged when something was extracted, even if some fields are wrong or null.

### What it does well

- Clean digital invoices and credit notes with a text layer: totals, dates, currency, vendor, line items.
- Currency inference from evidence (ISO code, unambiguous symbol, country, tax vocabulary); **null instead of a guess** when the evidence is missing or conflicting. Ambiguous numeric dates (`06/11/2023` with no locale evidence) are left null too.
- Self-checks: line total = quantity x price, subtotal + tax = total, line-item sum, IBAN checksum. Failed checks add `warnings`.
- Rejects non-invoices and unreadable photos **without charging**.
- Fast and cheap: a digital invoice takes well under a second of compute; an OCR page takes roughly 5-7 seconds.

### What it does poorly

- **Phone photos of receipts** (especially Indonesian and Malaysian thermal receipts, crumpled or angled): often missing vendor, tax or total.
- **Handwritten** invoices.
- Unusual layouts: vendor name can be wrong (a customer name or address chosen), invoice number can be a different reference, and the tax total is the most frequently missed field.
- Multi-column and highly styled invoices with line items in nested tables: line-item count can be off.
- Right-to-left and CJK documents have not been tested and should not be expected to work.

### Rejection, empty input and the free example

- **Not an invoice or receipt** (letters, contracts, menus, lists of invoices, press releases, plain text; automatic type mode only): the item has `error.code = "DOCUMENT_TYPE_NOT_RECOGNISED"`, **null fields, and is not charged**. Set `documentTypeCheck` to `warn` or `off`, or pick `documentType` yourself, to extract anyway.
- **Image too low quality** (blurred, tiny or unreadable photos): `error.code = "IMAGE_TOO_LOW_QUALITY"` with a plain message ("image too low quality to read reliably; try a higher-resolution scan"), **not charged**. Set `imageQualityCheck` to `warn` or `off` to see what the OCR makes of it anyway.
- **Empty input**: the run finishes successfully with the status message "No input given: add document URLs/uploads, paste text, give a dataset ID, or tick 'Use the built-in sample'. Nothing was processed or charged." and an empty dataset.
- **Built-in example**: leave "Use the built-in sample invoice" ticked (the default in the Console prefill) and press Start. It runs a bundled sample invoice in about 6 seconds and is **free**.

### Quick start

1. Leave **"Use the built-in sample invoice"** ticked and press **Start** to see the output (free).
2. Untick it, add files/URLs (or paste text), and run.

#### Input

| Field | Description |
|---|---|
| `fileUrls` | Uploaded files or public `http(s)` URLs (PDF, PNG/JPG/TIFF, DOCX, HTML, XLSX, CSV, TXT). 50 MB/file, 100 docs/run. |
| `documents` | Advanced: `{"url": "..."}`, `{"base64": "...", "filename": "inv.pdf"}` or `{"keyValueStoreId": "...", "key": "..."}`. |
| `text` | Pasted text or Markdown. |
| `datasetId` | Pick a dataset (for example a Document to Markdown run) whose items have a `markdown`/`text` field. |
| `documentType` | `auto` (default), `invoice`, `receipt`, `form`, `table`, `generic`. |
| `fieldSchema` | Your own fields, see below. |
| `outputMode` | `document` (nested, default), `flat` (one row per document), `lineItems`, `tableRows`. |
| `dateOrder`, `defaultCurrency` | Resolve `03/04/2026` and documents with no currency symbol. |
| `documentTypeCheck` | `reject` (default: in auto mode, non-invoices return `DOCUMENT_TYPE_NOT_RECOGNISED`, not charged), `warn`, or `off`. |
| `strictOcrTotals` | Default on: a lone, uncorroborated low-confidence OCR total becomes null. |
| `imageQualityCheck` | `reject` (default: unreadable photos return `IMAGE_TOO_LOW_QUALITY`, not charged), `warn`, or `off`. |
| `enableOcr`, `ocrLanguage` | OCR for scans and images (`eng`, `deu`, `fra`, `spa`, `ita`, `por`, `nld`, combine with `+`, or `auto` to detect). |
| `maxPages`, `includeTables`, `includeKeyValues`, `includeText` | Limits and extras. |

#### Custom fields (label / regex heuristics)

```json
{
  "fileUrls": ["https://example.com/purchase-order.pdf"],
  "documentType": "generic",
  "fieldSchema": [
    {"name": "poNumber", "labels": ["PO No", "Purchase Order"], "type": "string"},
    {"name": "shipDate", "labels": ["Ship date", "Delivery date"], "type": "date"},
    {"name": "orderRef", "regex": "REF-(\\d+)", "type": "integer"},
    {"name": "emails", "type": "email", "regex": "[\\w.+-]+@[\\w-]+\\.[\\w.]+", "regexGroup": 0, "multiple": true},
    {"name": "incoterms", "labels": ["Incoterms"], "required": true}
  ]
}
```

`labels` finds the value next to or under a label. `regex` runs on the document text (`regexGroup` picks the group). Types: `string`, `number`, `integer`, `currency`, `date` (returned as `YYYY-MM-DD`), `boolean`, `email`, `phone`, `percent`, `list`. A missing `required` field adds a warning and is listed in `missingRequired`.

#### Output (outputMode `document`)

```json
{
  "source": "built-in sample invoice",
  "ok": true,
  "documentType": "invoice",
  "pageCount": 1,
  "ocrPages": 0,
  "fields": {
    "invoiceNumber": "BW-2026-0187", "invoiceDate": "2026-10-02", "dueDate": "2026-11-01",
    "vendor": "Brightwave Design Studio LLC", "customer": "Cedar & Pine Outfitters",
    "subtotal": 4266.0, "taxTotal": 0.0, "total": 4266.0, "currency": "USD", "purchaseOrder": "PO-55120"
  },
  "confidence": {"invoiceNumber": "high", "total": "high", "vendor": "medium"},
  "lineItems": [
    {"description": "Website design (hours)", "quantity": 24.0, "unitPrice": 95.0, "amount": 2280.0, "amountCheck": true}
  ],
  "validation": {"lineItemsSum": 4266.0, "lineItemsMatchSubtotalOrTotal": true, "subtotalPlusTaxEqualsTotal": true},
  "warnings": [],
  "charged": false
}
```

### Integrations

#### Email attachment → Google Sheets row (Zapier)

1. **Gmail** trigger: *New Attachment*.
2. **Apify** action *Run Actor*: Actor `steady_kahikatea/document-data-extractor`, input:
   ```json
   {"fileUrls": ["{{attachment URL}}"], "outputMode": "flat", "documentType": "invoice"}
   ```
3. **Apify** action *Get Dataset Items* (use the run's default dataset).
4. **Google Sheets** action *Create Spreadsheet Row*: map `vendor`, `invoiceNumber`, `invoiceDate`, `currency`, `total`, `taxTotal`, `warnings`.

Use `outputMode: "lineItems"` instead to add one Sheets row per invoice line.

#### Make

*Gmail → Watch emails* (attachments on) → *Apify → Run an Actor* (or *HTTP → Make a request* to `https://api.apify.com/v2/acts/steady_kahikatea~document-data-extractor/run-sync-get-dataset-items?token=YOUR_TOKEN` with body `{"documents":[{"base64":"{{base64(attachment.data)}}","filename":"{{attachment.fileName}}"}],"outputMode":"flat"}`) → *Google Sheets → Add a row*. The response is the list of rows.

#### n8n

*Gmail Trigger* (download attachments) → *Code* node to base64 the binary → *HTTP Request* `POST https://api.apify.com/v2/acts/steady_kahikatea~document-data-extractor/run-sync-get-dataset-items` (header `Authorization: Bearer <token>`) with JSON body `{"documents":[{"base64":"={{$binary.attachment_0.data}}","filename":"={{$binary.attachment_0.fileName}}"}],"outputMode":"flat"}` → *Google Sheets* *Append row*. Or use the Apify node's *Run Actor and get dataset* operation with a public file URL.

#### Python / LangChain

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("steady_kahikatea/document-data-extractor").call(run_input={
    "fileUrls": ["https://example.com/invoice.pdf"],
    "documentType": "invoice",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["fields"]["vendor"], item["fields"]["total"], item["warnings"])
```

As a LangChain tool (`pip install langchain-apify`):

```python
from langchain_apify import ApifyActorsTool

extract = ApifyActorsTool("steady_kahikatea/document-data-extractor")
result = extract.invoke({"run_input": {"fileUrls": ["https://example.com/invoice.pdf"], "outputMode": "flat"}})
```

Chain it after **Document to Markdown**: pass that run's dataset in `datasetId`, or paste its Markdown into `text`.

#### API (cURL)

```bash
curl -X POST "https://api.apify.com/v2/acts/steady_kahikatea~document-data-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileUrls":["https://example.com/invoice.pdf"],"outputMode":"flat"}'
```

### Pricing (pay per event)

You pay only for documents that produced extracted data. Rejected, unreadable, not-an-invoice, blocked, failed and empty documents and the built-in sample are free.

| Event | Free / no discount | Bronze (-10%) | Silver (-20%) | Gold (-30%) |
|---|---|---|---|---|
| **Document extracted** (first 5 pages included) | $0.012 | $0.0108 | $0.0096 | $0.0084 |
| **OCR page** (each scanned page or image that needed OCR) | $0.008 | $0.0072 | $0.0064 | $0.0056 |
| **Extra page** (each page beyond 5) | $0.003 | $0.0027 | $0.0024 | $0.0021 |

Examples (no discount): a digital invoice PDF = $0.012; a one-page invoice photo or scan = $0.012 + $0.008 = $0.020; a 1,000-invoice digital batch = $12. Platform usage is included in the event prices.

### Limits and safety

- Public `http(s)` URLs only. Private/internal addresses, `localhost`, cloud metadata addresses and credentials in URLs are blocked, on every redirect hop.
- 50 MB per file, 100 documents per run, 500 pages per document, 4 minutes of processing per document.
- DOCX headers and footers are not read. Password-protected PDFs are not supported.
- Documents are processed in memory only. Contents are never written to logs.
- Not tax, legal or accounting advice: verify extracted amounts before posting them to your books.

### Open-source alternative

[invoice2data](https://github.com/invoice2data/invoice2data) (MIT) is a free library for template-based invoice extraction that you host yourself. This Actor needs no templates, adds OCR, forms, tables and custom fields, and runs without hosting.

# Actor input Schema

## `useSampleDocument` (type: `boolean`):

Run on a bundled sample invoice (free, ignored when you give your own documents). Untick it when you add your own files, URLs or text.

## `fileUrls` (type: `array`):

PDF, scanned PDF, PNG/JPG/TIFF image, DOCX, HTML, XLSX, CSV or text. Public HTTP(S) URLs only. Max 50 MB per file, 100 documents per run.

## `documents` (type: `array`):

Array of objects for API/automation use: {"url":"https://..."}, {"base64":"<file bytes>","filename":"invoice.pdf"} (handy for n8n/Make binary attachments), or {"keyValueStoreId":"...","key":"..."}.

## `text` (type: `string`):

Plain text or Markdown to extract from (for example the output of the Document to Markdown Actor, or an email body).

## `datasetId` (type: `string`):

Pick a dataset whose items contain a text field (e.g. the output of the Document to Markdown Actor). Each item with text is extracted as one document. The Actor only gets access to the dataset you pick.

## `datasetTextField` (type: `string`):

Field with the document text. Falls back to markdown, text, content, body.

## `documentType` (type: `string`):

auto detects invoice / receipt / form / table. Force a type if detection is wrong.

## `fieldSchema` (type: `array`):

Your own fields, found by label and/or regex. Each item: {"name":"poNumber","labels":\["PO No","Purchase Order"],"type":"string"} or {"name":"ref","regex":"REF-(\d+)"}. Types: string, number, integer, currency, date, boolean, email, phone, percent, list. Optional: multiple, required, default, regexGroup.

## `outputMode` (type: `string`):

document: one nested record per document. flat: one flat row per document (Google Sheets / Airtable). lineItems: one row per invoice line. tableRows: one row per table row.

## `includeTables` (type: `boolean`):

Detect tables and return them as rows. Default: on for forms/tables/generic documents, off for invoices and receipts (their line items are returned separately).

## `includeKeyValues` (type: `boolean`):

For forms and generic documents: return every 'Label: value' pair found.

## `includeText` (type: `boolean`):

Add the plain text the extraction ran on (helps debugging layouts).

## `dateOrder` (type: `string`):

How to read 03/04/2026. Auto uses document language hints.

## `defaultCurrency` (type: `string`):

ISO code (USD, EUR, GBP...) used when the document shows no currency symbol or code.

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

Run Tesseract OCR on images and PDF pages without a text layer. Billed per OCR'd page. Slower and less accurate than digital PDFs.

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

Tesseract language codes (eng, deu, fra, spa, por, ita, nld), combined with +, e.g. deu+eng. Or 'auto': read with English first, detect the language from the text and re-read with that pack (helps accented names; on our test photos it was on par with plain English, so the default stays eng).

## `imageQualityCheck` (type: `string`):

reject (default): photos and scans that cannot be read reliably (blurred, tiny text, very low contrast, OCR confidence far too low) fail with IMAGE_TOO_LOW_QUALITY and are not charged, instead of returning guesses. warn: process anyway and add a warning. off: no check.

## `documentTypeCheck` (type: `string`):

In automatic document type mode, documents with no invoice or receipt evidence (letters, contracts, menus, press releases, lists of invoices, plain text) return DOCUMENT_TYPE_NOT_RECOGNISED with no fields and no charge instead of invented values. 'warn' returns best-effort output with a warning; 'off' disables the check. The check never applies when you choose a documentType, give a fieldSchema, or process spreadsheets/CSV.

## `strictOcrTotals` (type: `boolean`):

On photos and scans, a total that is printed once, is not a clearly labelled total and does not agree with the line items or subtotal + tax is returned as null (with a warning) instead of a probably misread number. Turn off to always get the best guess.

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

Pages beyond this are ignored.

## Actor input object example

```json
{
  "useSampleDocument": true,
  "datasetTextField": "markdown",
  "documentType": "auto",
  "outputMode": "document",
  "includeKeyValues": true,
  "includeText": false,
  "dateOrder": "auto",
  "enableOcr": true,
  "ocrLanguage": "eng",
  "imageQualityCheck": "reject",
  "documentTypeCheck": "reject",
  "strictOcrTotals": true,
  "maxPages": 50
}
```

# Actor output Schema

## `results` (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 = {
    "useSampleDocument": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("steady_kahikatea/document-data-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 = { "useSampleDocument": True }

# Run the Actor and wait for it to finish
run = client.actor("steady_kahikatea/document-data-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 '{
  "useSampleDocument": true
}' |
apify call steady_kahikatea/document-data-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steady_kahikatea/document-data-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/elgFHBqucQCH68fLb/builds/KV8kagDfR0gQJh2dC/openapi.json
