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

Invoice, Receipt & PDF Table to JSON/Markdown OCR

Pricing

$15.00 / 1,000 pdf document parseds

Go to Apify Store
Invoice, Receipt & PDF Table to JSON/Markdown OCR

Invoice, Receipt & PDF Table to JSON/Markdown OCR

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

Pricing

$15.00 / 1,000 pdf document parseds

Rating

0.0

(0)

Developer

Panix AI LAB

Panix AI LAB

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

18 hours ago

Last modified

Categories

Share

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

FieldTypeDefaultDescription
documentUrlsarray of stringsrequiredDirect links to PDFs or images. Duplicates are removed. Objects like {"url": "..."} are accepted too.
ocrModeauto / always / neverautoHow OCR is used, see below.
extractTablesAsJsonbooleantrueExport tables as JSON in tables_json. Tables are always included in the Markdown.
maxPagesinteger (1-2000)100Parse at most this many pages per document, starting from page 1.
maxFileSizeMbinteger (1-200)50Larger files are rejected before parsing.
maxDocumentsinteger (1-1000)100Process at most this many URLs per run; extra URLs are ignored with a warning.
documentTimeoutSecsinteger (30-3600)300Time limit per document; pages finished so far are returned as a partial result.

Example input:

{
"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.

FieldDescription
source_urlThe URL from your input.
file_nameFile name taken from the URL.
document_typepdf or image (null if the file could not be downloaded or recognised).
statussuccess, partial_success (some pages missing, see warnings), failed or skipped (see error_message).
error_messageWhy the document failed or was skipped; null on success.
warningsNon-fatal notes, for example pages cut by maxPages or a timeout.
ocr_modeThe OCR mode used.
pages_totalPages (or image frames) in the file.
pages_processedPages parsed in this run.
pages_digitalPages parsed from the text layer, without OCR.
pages_ocrPages parsed with OCR.
table_countNumber of tables in tables_json.
tables_jsonTables with table_index, page_number, headers, rows, row_count, column_count.
full_markdownMarkdown of the processed pages.
markdown_urlSet only for very large documents: link to the Markdown file in the key-value store (then full_markdown is null).
tables_urlSet only when the tables alone are too large for a dataset item: link to the tables JSON (then tables_json is empty).
markdown_lengthCharacters in the Markdown.
processing_time_secsSeconds spent on this document.
processed_atUTC 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):

{
"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:

{
"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>):

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:

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.