Invoice Data Extractor: PDF & Image to JSON (No LLM)
Pricing
Pay per event
Invoice Data Extractor: PDF & Image to JSON (No LLM)
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.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Nick Hellmich
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
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
confidenceandwarningsfields 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/2023with 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. SetdocumentTypeChecktowarnoroff, or pickdocumentTypeyourself, 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. SetimageQualityChecktowarnoroffto 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
- Leave "Use the built-in sample invoice" ticked and press Start to see the output (free).
- 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)
{"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)
{"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)
- Gmail trigger: New Attachment.
- Apify action Run Actor: Actor
steady_kahikatea/document-data-extractor, input:{"fileUrls": ["{{attachment URL}}"], "outputMode": "flat", "documentType": "invoice"} - Apify action Get Dataset Items (use the run's default dataset).
- 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
from apify_client import ApifyClientclient = 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):
from langchain_apify import ApifyActorsToolextract = 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)
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 (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.