PDF to JSON Extractor — Tables, Fields & Structure
Pricing
from $1.40 / 1,000 page extractions
PDF to JSON Extractor — Tables, Fields & Structure
Input: PDF URLs, many per run. Output: one row per document — tables as rows and columns, text in reading order, Markdown for RAG, metadata, outline, form fields, key fields (invoice no., dates, totals, IBAN, VAT). No OCR: a scanned page is flagged and not charged.
Pricing
from $1.40 / 1,000 page extractions
Rating
0.0
(0)
Developer
Power On Labs
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 days ago
Last modified
Categories
Share
Turn a PDF into structured JSON: tables as real rows and columns, headings and paragraphs in true reading order, document metadata, form fields, and the key values you actually want — invoice number, dates, totals, IBAN, VAT ID.
Most PDF extractors hand back a wall of text. That is fine if a human is going to read it. It is useless if a script has to find the total of an invoice, or if a retrieval system has to keep a financial table intact. This Actor keeps the structure.
{ "pdfUrls": ["https://example.com/invoice.pdf"] }
What comes back
| Tables as rows and columns | Every table becomes a 2-D array with a detected header row, plus the page and the bounding box it came from. Borderless tables are found too: detection reads how the text lines up, not the drawn rules — so the tables that line-based tools miss come through. |
| True reading order | A two-column paper, a newsletter, a report with a sidebar: the text comes out in the order a person reads it, not in the order the file happens to draw it. Recursive whitespace segmentation separates columns from full-width headings before anything is read. |
| Headings and paragraphs | Blocks are typed — heading (with a level), paragraph, listItem — using the document's own type scale, so an 8 pt body with 11 pt titles is read as well as a 11 pt body with 24 pt titles. |
| Key fields, checked | Invoice and order numbers, dates, totals and amounts with their currency, emails, phones, IBANs, VAT IDs. IBANs are validated with the mod-97 checksum and Italian VAT numbers with their check digit, so an invalid one is dropped instead of returned. |
| Dates resolved, or flagged | 03/04/2026 is 3 April in Europe and 4 March in the US. If the document elsewhere shows its convention, the date is resolved and marked assumedOrder. If it does not, iso stays null and both readings are returned. A wrong due date is worse than a missing one. |
| Form fields | AcroForm fields with names, types and filled-in values — for W-9s, tax forms, applications, contracts. A checkbox is reported as checkbox and a radio group as radio, not as the button the PDF format calls them both. |
| Metadata and bookmarks | Title, author, subject, creator, producer, creation and modification dates as ISO timestamps, page count, PDF version, encryption and signature flags, plus the bookmark tree — every heading with its nesting level and the page it points to. |
| Markdown for RAG | The same structure rendered as Markdown, headings and pipe tables included. A table pasted as flat text is noise inside a retrieval system; the same table in Markdown stays queryable. |
| Scanned PDFs are labelled, not faked | A page with no text layer is reported as such (isScanned, needsOcr) and is not charged. See the limits below. |
Pricing — you pay for pages, and only the ones that worked
$0.002 per page extracted. Nothing else. No start fee, no per-document fee.
- A page that has no text layer (a scan) is not charged.
- A file that fails — dead link, wrong password, not a PDF — is not charged.
- Pages skipped with
firstPage/maxPagesare not charged. - At most 40 pages are charged per document. A 500-page manual costs the same as a 40-page one: $0.08. Long documents stay predictable.
A five-page invoice costs one cent.
Quick start
Minimal input:
{ "pdfUrls": ["https://example.com/invoice.pdf"] }
With options:
{"pdfUrls": [{ "url": "https://example.com/statement.pdf", "password": "hunter2", "name": "march-statement" },"https://example.com/report.pdf"],"detectTables": true,"extractKeyFields": true,"outputFormats": ["json", "markdown", "csv"],"maxPages": 20}
From the JavaScript client:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<APIFY_TOKEN>' });const run = await client.actor('power_on/pdf-to-json-extractor').call({pdfUrls: ['https://example.com/invoice.pdf'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items[0].fields.totals); // [{ label: 'total due', value: 1220, currency: 'EUR' }]console.log(items[0].tables[0].rows); // [['Item', 'Qty', 'Price'], ['Widget', '2', '9.90']]
From Python:
from apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("power_on/pdf-to-json-extractor").call(run_input={"pdfUrls": ["https://example.com/invoice.pdf"]})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["fields"], item["tables"])
It also works with no code at all through Apify's Zapier, Make and n8n integrations, or straight from the REST API.
Where the PDFs can come from
- A public URL — the ordinary case.
- A private endpoint — put an
Authorizationheader in Extra HTTP headers. - A file you have on disk — upload it to an Apify key-value store and pass the record
URL, or inline it as a base64 data URI:
data:application/pdf;base64,JVBERi0xLjQK….
What comes out
One dataset row per input file, plus the files themselves in the key-value store. Row shape:
{"url": "https://example.com/invoice.pdf","ok": true,"pageCount": 3,"pagesParsed": 3,"pagesWithText": 3,"pagesCharged": 3,"isScanned": false,"needsOcr": false,"tableCount": 2,"metadata": { "title": "Invoice 42", "author": "Acme", "createdAt": "2026-03-14T09:12:00.000Z", "pageCount": 3, "encrypted": false, "hasAcroForm": false },"fields": {"identifiers": { "invoiceNumber": "INV-2026-0042", "orderNumber": "PO-99812" },"totals": [{ "label": "total due", "value": 1220, "currency": "EUR", "raw": "EUR 1.220,00" }],"dates": [{ "raw": "03/04/2026", "iso": "2026-04-03", "assumedOrder": "day-first", "readings": ["2026-04-03", "2026-03-04"] }],"ibans": ["IT60X0542811101000000123456"],"emails": ["billing@acme.example"]},"tables": [{"page": 2,"rowCount": 4,"columnCount": 3,"header": ["Item", "Qty", "Price"],"rows": [["Item", "Qty", "Price"], ["Widget", "2", "9.90"]],"bbox": { "x0": 72, "y0": 310, "x1": 523, "y1": 402 }}],"pages": [{"page": 1,"width": 595.3,"height": 841.9,"rotation": 0,"hasTextLayer": true,"text": "…","blocks": [{ "type": "heading", "level": 1, "text": "Invoice", "bbox": { "x0": 72, "y0": 60, "x1": 240, "y1": 84 } }],"tables": []}],"jsonUrl": "https://api.apify.com/v2/key-value-stores/…/records/001-invoice.json?signature=…","markdownUrl": "…"}
rows always contains every row of the table, header included; header is a convenience
copy of the header row when one was detected.
The file links are signed, so they open in a browser or from curl without a token.
When a file goes wrong
One broken PDF never takes down the run. Every input gets a row, and a failed one carries
ok: false with an error and an errorCode:
errorCode | What happened |
|---|---|
PASSWORD_REQUIRED | The PDF is encrypted and no password was given |
PASSWORD_WRONG | The password given does not open it |
INVALID_PDF | Truncated, corrupt, or the URL returned an HTML page instead of a file |
EXTRACTION_FAILED | Network failure, timeout, size limit, HTTP error |
A page that fails on its own is recorded in pageErrors and the rest of the document is
still extracted.
Known limits — read this before you buy
- No OCR. A scanned PDF has no text layer, and this Actor does not run optical
character recognition on it. It detects the situation, returns
isScanned: trueandneedsOcr, and does not charge you for those pages. If your documents are scans, you need an OCR tool, not this one. - Formulas and code listings can occasionally be reported as small tables.
- Tables split across a page break come back as two tables, one per page.
- Right-to-left scripts are extracted but the reading order within a line follows the file's own text order.
These are stated on purpose. It is cheaper for both of us if you find out here rather than after a run.
Everything you can set
| Option | Default | What it does |
|---|---|---|
pdfUrls | — | URLs, {url, password, name} objects, or base64 data URIs |
detectTables | true | Find tables and return them as rows and columns |
extractKeyFields | true | Invoice numbers, dates, totals, IBAN, VAT, emails, phones |
includeFormFields | true | AcroForm fields with their values |
includeLinks | true | Hyperlink annotations per page |
includeLines | false | Every text line with its bounding box |
outputFormats | ["json","markdown"] | Also text and csv |
password | "" | Applied to files that carry no password of their own |
firstPage / maxPages | 1 / 0 | Read a slice of the document; skipped pages are free |
inlineFullResult | "auto" | Embed the full structure in the dataset row, or link to it |
concurrency | 3 | Documents in parallel |
retries | 2 | Retries on network failures only |
timeoutSecs | 120 | Download timeout |
maxFileSizeMb | 100 | Larger files are refused with an explanation |
headers | {} | Extra HTTP headers for private endpoints |
proxyConfiguration | off | For hosts that block datacenter addresses |
Good uses
Invoice and receipt automation · feeding contracts and reports into a RAG index · pulling financial tables out of annual reports and statements · reading filled-in tax and application forms · turning research papers into clean Markdown · migrating a document archive into a database.
Also from Power On Labs
- Website Screenshot & PDF Generator — the other direction: a web page turned into a PDF or an image. Feed its output back here if you want that PDF parsed.
- Sitemap URL Extractor — every page URL a site publishes, filtered by pattern. Useful for finding the PDFs a site hosts before extracting them.
- Tech Stack Audit — what a website is built with, with the evidence for every detection.
Built by Power On Labs. Something extracted wrong? Open an issue on the Actor with the PDF or a link to it and it gets fixed — the extraction rules are ours, not a third-party library's default settings.