PDF Table to Validated Rows (CSV, Excel, JSON) avatar

PDF Table to Validated Rows (CSV, Excel, JSON)

Pricing

from $2.00 / 1,000 accepted rows

Go to Apify Store
PDF Table to Validated Rows (CSV, Excel, JSON)

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

Jordan Nabbe

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

13 days ago

Last modified

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 2 or a printed URL are skipped, and so is text that follows the table after a clear gap. The number of skipped lines is reported as linesIgnored.
  • 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, QTY and Aantal can all become quantity. Matching ignores case by default.
  • Never guesses notation. 1,234.56, 1.234,56, 1 249,00, € 12,50 and (12.50) are read exactly. A value such as 1.000 means something different in US and European notation. It is resolved only when the same document settles it, for example when another price reads 0,84. Otherwise the row is rejected with AMBIGUOUS_NUMBER.
  • Settles dates from evidence. A date such as 03/04/2026 is resolved when you set dateFormat, or when the same document contains an unambiguous date such as 25/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 as DMY-inferred-from-number-format. US notation never implies month-day order, so such dates stay rejected with AMBIGUOUS_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, conflict or none).
  • 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 OUTPUT record 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

  1. 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.
  2. Replace documents with your own. Each document needs a unique id and exactly one source:
    • base64Document: the PDF as standard base64.
    • csv or text, with an optional delimiter.
    • json, html or records.
    • keyValueStoreId plus keyValueStoreKey: a binary PDF in one of your named Apify key-value stores.
  3. Describe your columns in targetSchema. If one supplier always uses a fixed notation, set numberFormat or dateFormat on that document or in options.

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:

skuquantityunit_priceline_totalsourceDocumentsourcePagesourceTablesourceRowextractionMethodrowStatus
A-100212.525northwind-prices111pdf-text-layerACCEPTED
C-30041.255northwind-prices113pdf-text-layerACCEPTED

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 extraction block 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.

EventPrice (USD)Charged when
Actor start (apify-actor-start)$0.005Once per run when the Actor starts.
Accepted table row (apify-default-dataset-item)$0.002An 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 uniqueBy field, otherwise the first required column), and wrapped lines join the nearest anchored row. A text item that overlaps two columns equally rejects its row with AMBIGUOUS_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 url documents fail with PUBLIC_SOURCE_NOT_APPROVED and no request is made.
  • Schema: up to 32 columns, 32 aliases per column, 8 uniqueBy fields 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.