PDF Table to Validated Rows (CSV, Excel, JSON)
Pricing
from $2.00 / 1,000 accepted rows
PDF Table to Validated Rows (CSV, Excel, JSON)
Extract the table from supplier PDFs (price lists, order confirmations, stock reports) into flat, validated rows: header aliases, European and US numbers and dates, required fields, duplicates and line-total checks. Rejected rows come back with reasons; you pay only for accepted rows.
Pricing
from $2.00 / 1,000 accepted rows
Rating
0.0
(0)
Developer
Jordan Nabbe
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 days ago
Last modified
Categories
Share
PDF Table to Validated Rows
Purchasing, finance-operations and automation teams get the same supplier PDFs every week: price lists, order confirmations and stock reports. PDF Table to Validated Rows turns the table in each native-text PDF into clean, flat, typed rows for a spreadsheet, database, ERP import or LLM pipeline. Your own column names are used even when the header labels change between suppliers. US and European numbers and dates are read without guessing. Required fields, duplicate keys and line totals are checked. Every row carries its document, page, table and row number. Rows that fail go to a separate exception list with machine-readable reasons, so bad data stays out of your import and you pay only for rows that pass.
What it does
- Parses real PDF bytes. Send a PDF as base64. The Actor opens it with unpdf (PDF.js), reads the native text layer with page coordinates, finds the header line that matches your columns and rebuilds the table under it.
- Handles real-world layouts. It copes with multi-page tables with or without repeated headers, descriptions that wrap onto a second line, and cells that are vertically centred next to a wrapped description. Page footers such as
1/2,Page 1 of 2or a printed URL are skipped, and so is text that follows the table after a clear gap. The number of skipped lines is reported aslinesIgnored. - Validates inline tables too. CSV, delimited text, JSON, HTML tables and plain records go through the same schema. If you don't set a delimiter, it is detected (comma, semicolon, tab or pipe).
- Maps header aliases.
Qty,QTYandAantalcan all becomequantity. Matching ignores case by default. - Never guesses notation.
1,234.56,1.234,56,1 249,00,€ 12,50and(12.50)are read exactly. A value such as1.000means something different in US and European notation. It is resolved only when the same document settles it, for example when another price reads0,84. Otherwise the row is rejected withAMBIGUOUS_NUMBER. - Settles dates from evidence. A date such as
03/04/2026is resolved when you setdateFormat, or when the same document contains an unambiguous date such as25/04/2026. If there is no date evidence but the document uses European number notation (a decimal comma), day-month-year order is applied and reported asDMY-inferred-from-number-format. US notation never implies month-day order, so such dates stay rejected withAMBIGUOUS_DATE. Dates written with month names (3 okt 2026,Oct 3, 2026) are read directly. Each document reports the notation it used and where it came from (option,document,number-format,conflictornone). - Never rounds money. Values that would lose decimal precision are rejected with
PRECISION_LOSS. Arithmetic checks use exact decimals. - Checks business rules: required fields, uniqueness keys and arithmetic such as quantity × unit price = line total, within your tolerance.
- Keeps results and exceptions apart. Accepted rows go to the dataset. The
OUTPUTrecord holds rejected rows with their values, reasons and provenance, plus header mappings, detected notation, document errors and counts. - Fails transparently. Scanned, encrypted, rotated or unreadable PDFs get a per-document error code instead of plausible but wrong rows. The other documents in the run carry on.
How to use it
- Click Start with the prefilled input. It is a real one-page supplier price list PDF with three rows. Two rows pass. One is rejected because 3 × 8.00 is not 20.00.
- Replace
documentswith your own. Each document needs a uniqueidand exactly one source:base64Document: the PDF as standard base64.csvortext, with an optionaldelimiter.json,htmlorrecords.keyValueStoreIdpluskeyValueStoreKey: a binary PDF in one of your named Apify key-value stores.
- Describe your columns in
targetSchema. If one supplier always uses a fixed notation, setnumberFormatordateFormaton that document or inoptions.
This copy-paste example uses European notation and runs fully offline:
{"documents": [{"id": "supplier-prices","csv": "SKU;Qty;Unit Price;Line Total\nA-100;2;12,50;25,00\nB-200;3;8,00;20,00","delimiter": ";"}],"targetSchema": {"columns": [{ "name": "sku", "type": "string", "required": true, "aliases": ["SKU", "Item Code", "Artikel"] },{ "name": "quantity", "type": "integer", "required": true, "aliases": ["Qty", "Aantal"] },{ "name": "unit_price", "type": "number", "required": true, "aliases": ["Unit Price", "Prijs"] },{ "name": "line_total", "type": "number", "required": true, "aliases": ["Line Total", "Totaal"] }],"uniqueBy": ["sku"],"arithmeticChecks": [{ "operands": ["quantity", "unit_price"], "operator": "multiply", "result": "line_total", "tolerance": 0.01 }]},"options": { "numberFormat": "eu" }}
To read only some pages of a PDF, add "pageRange": { "from": 2, "to": 3 } to that document.
Output
Accepted rows are stored flat in the default dataset. You can export them as JSON, CSV or Excel. These are the rows from the prefilled PDF:
| sku | quantity | unit_price | line_total | sourceDocument | sourcePage | sourceTable | sourceRow | extractionMethod | rowStatus |
|---|---|---|---|---|---|---|---|---|---|
| A-100 | 2 | 12.5 | 25 | northwind-prices | 1 | 1 | 1 | pdf-text-layer | ACCEPTED |
| C-300 | 4 | 1.25 | 5 | northwind-prices | 1 | 1 | 3 | pdf-text-layer | ACCEPTED |
The failing row appears in OUTPUT.rejectedRows, never in the dataset:
{"data": { "sku": "B-200", "quantity": 3, "unit_price": 8, "line_total": 20 },"sourceDocument": "northwind-prices", "sourcePage": 1, "sourceTable": 1, "sourceRow": 2,"extractionMethod": "pdf-text-layer","errors": [{ "code": "ARITHMETIC_CHECK_FAILED", "field": "line_total", "message": "Arithmetic check failed.", "expected": "24", "received": "20", "tolerance": "0.01" }]}
Each entry in OUTPUT.documents lists the following:
- pages read
- header mapping, plus any missing and unexpected columns
linesIgnored- warnings
- an
extractionblock such as{ "numberFormat": "en", "numberFormatSource": "document", "dateFormat": null, "dateFormatSource": "none", "delimiter": null } - an error code if the document failed
OUTPUT.outcome holds the status (SUCCEEDED, PARTIAL, FAILED or LIMIT_REACHED) and the delivery counts.
Pricing
Pay per event. The main charge is per accepted table row; everything else listed below is free.
| Event | Price (USD) | Charged when |
|---|---|---|
Actor start (apify-actor-start) | $0.005 | Once per run when the Actor starts. |
Accepted table row (apify-default-dataset-item) | $0.002 | An accepted row is delivered to the default dataset. |
Not charged:
- Rejected rows and validation errors
- Document failures, header mappings and detected notation
- Summary counts, spending-limit diagnostics, and strict-mode diagnostics
Examples (default 512 MB memory, one start event):
- Twenty accepted rows: $0.005 start + 20 × $0.002 = $0.045
- No accepted rows: $0.005 start + 0 × $0.002 = $0.005
Spending limit: when you set a maximum cost per run, the Actor stops before it would exceed it, keeps every accepted table row it already delivered and reports limitReached in the OUTPUT record. It never delivers results beyond the limit.
The Store's Pricing tab shows the prices in force. If this section and the Pricing tab ever differ, the Pricing tab applies.
Use cases
- Check supplier price lists before importing them into an ERP or webshop.
- Normalize changing column names across monthly stock or sales reports.
- Verify quantity × price = total on order confirmations before approval.
- Send only the rejected rows to a reviewer instead of checking every page.
- Apply one schema to native PDFs and to tables already extracted as CSV, JSON or HTML.
- Feed LLM or analytics pipelines with typed rows that carry their page and row reference.
Limits
- Per run: up to 50 documents and 1,000 table rows in total. A document that would go over the row limit fails with
ROW_LIMIT_EXCEEDED. Rows from earlier documents are kept. - Size: 2 MiB per source and 6 MiB of input JSON.
- Time: each document has 30 seconds, and no new document starts after 120 seconds.
- PDFs: up to 25 pages. Text must be horizontal and native. A page needs a header line with at least two of your column labels (one if the schema has a single column). A page without a header continues with the previous page's columns. Rows are anchored on your key column (the first
uniqueByfield, otherwise the first required column), and wrapped lines join the nearest anchored row. A text item that overlaps two columns equally rejects its row withAMBIGUOUS_TABLE_LAYOUT. - Dates: four-digit years only. Two-digit years are rejected as
INVALID_TYPE. - Not supported: OCR of scanned PDFs (
SCANNED_PDF_REQUIRES_OCR), password-protected PDFs (ENCRYPTED_PDF), rotated text, side-by-side tables and Excel files. Accuracy is not guaranteed for every layout. - Remote URLs: no public PDF host has been approved, so
urldocuments fail withPUBLIC_SOURCE_NOT_APPROVEDand no request is made. - Schema: up to 32 columns, 32 aliases per column, 8
uniqueByfields and 16 arithmetic checks.
FAQ
Does it really parse PDFs offline? Yes. The prefilled input is a real PDF encoded as base64, and it is parsed without network access.
Am I charged for rejected rows or failed documents? No. Only accepted rows delivered to the dataset are billable.
How does it decide between 1.000 and 1,000? It only uses evidence from the same document. If some prices can only be read one way, that notation settles the ambiguous values, and the document reports numberFormatSource: document. If the document contains both notations or no evidence, ambiguous values are rejected.
How does it decide between 3 April and 4 March? In this order: your dateFormat setting, then unambiguous dates in the same document, then European number notation in the same document (reported as DMY-inferred-from-number-format). If none of these applies, or the document contains conflicting dates, the row is rejected with AMBIGUOUS_DATE. Set dateFormat to override the inference.
What does PARTIAL mean? At least one row was delivered, but some rows or documents failed. Details are in OUTPUT.rejectedRows and OUTPUT.documents.
Can I require every row to pass? Set options.failOnAnyRejectedRow to true. If any row is rejected, all accepted rows are withheld and the run fails.
What happens at my spending limit? Rows are delivered only while the limit allows. OUTPUT.outcome.limitReached is true, and undelivered counts the rows held back.
Is my data logged or kept? The log shows counts only. Nothing is kept between runs. Only submit documents you are authorized to process, and leave out personal, medical or financial details you do not need.