PDF Extractor: Bulk PDF to Text, Tables & Markdown from a CSV avatar

PDF Extractor: Bulk PDF to Text, Tables & Markdown from a CSV

Pricing

from $7.00 / 1,000 documents

Go to Apify Store
PDF Extractor: Bulk PDF to Text, Tables & Markdown from a CSV

PDF Extractor: Bulk PDF to Text, Tables & Markdown from a CSV

Extracts text, tables and metadata from every PDF linked in an Apify dataset, CSV, Excel or Google Sheet, keeping your original columns. Inputs: datasetId or fileUrl or pdfUrls, urlField. Scans with no text layer are flagged, not guessed. Charged per document. Agent-ready: x402, MCP.

Pricing

from $7.00 / 1,000 documents

Rating

0.0

(0)

Developer

Adam Pearce

Adam Pearce

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 hours ago

Last modified

Share

Dataset PDF Extract: text and tables from every PDF in your list

Got a spreadsheet of links to invoices, statements, contracts, reports or filings, and no way to get what is inside them into columns?

Point this Actor at an Apify dataset, a CSV or Excel file, or a Google Sheet. For every row it downloads the PDF, reads the text, rebuilds the tables, pulls the document's own metadata, and hands you back every original column with the extracted fields added alongside. No copying, no pasting, no re-joining results to your own data afterwards.

Every other PDF extractor on the Store takes a typed-in list of URLs and gives you back a bare list of text. If your links live in a spreadsheet, you have to pull the column out, paste it in, and stitch the answers back together by hand. This one takes the whole sheet.

What you get per document

FieldWhat it is
textThe document's text in reading order, or Markdown with the tables appended
tablesEach table rebuilt as rows of cells, with its page number
pageCount, wordCount, charCount, tableCountSize of what was read
hasTextLayer, ocrRequiredWhether there was any text to read at all
pdfTitle, pdfAuthor, pdfSubject, pdfCreator, pdfProducerThe PDF's own metadata
pdfCreatedOn, pdfModifiedOnWhen the document was made and last changed
extractStatus, statusDetailWhat happened, in plain English, when it did not work
fileSizeKb, resolvedUrl, textTruncatedUseful when a link redirects or a document is enormous

Plus every column that was already in your row.

Tables are the point

Plain text extraction destroys table structure. A line-items table comes out as a stream of words with no idea which number belonged to which row.

This Actor reads the position of every piece of text on the page and rebuilds the grid: cells clustered into rows by their baseline, columns found by where cells actually line up across several rows. You get real rows of cells back, and with textFormat: markdown you also get proper Markdown tables appended to the text, which is the friendliest possible shape to hand to an LLM.

It is a best-effort reading of the printed layout, not a recovery of the original spreadsheet, and the README says that rather than pretending otherwise. Two deliberate decisions keep the output trustworthy:

  • Dot leaders are ignored. A printed form joins a label to its box with a run of full stops, and each one arrives as a separate piece of text. Left alone they invent a column per dot: one real tax form produced a 27-column "table" made almost entirely of punctuation.
  • Columns of prose are not a table. A two-column page layout is geometrically identical to a two-column table. The difference is that table cells hold values and prose holds sentences, so cell length is used to tell them apart. On a real government form this cut 35 reported "tables" down to 13 genuine ones without touching the real tables in an academic paper.

Set minTableRows to 2 if your invoices have a single line item, or higher to report only substantial tables.

The honest limit: scanned documents

A scanned or photographed PDF has no text layer. There is nothing to read, and this Actor will not pretend otherwise.

When that happens the row comes back with extractStatus: "no_text_layer", hasTextLayer: false and ocrRequired: true, so the fact travels with your data instead of living only on this page. Reading those documents needs OCR, which this Actor does not do.

Those documents are charged at the low scanned-document rate, because downloading and parsing them is real work and "this one needs OCR" is a real answer. Everything that returned nothing usable is not charged at all: a broken link, a 404, an HTML page instead of a file, a password-protected document, a corrupt file, a document over your size limit, or a blank cell.

Inputs

Give it any one of these:

  • Dataset picked from your account, for example a scraper's output
  • File or Google Sheet URL: CSV, TSV, Excel, JSON or JSON Lines. A normal Google Sheets link works, shared as "Anyone with the link can view"
  • PDF URLs: a plain list, for a quick one-off
  • Rows: inline JSON

The link column is detected automatically, preferring a column whose values end in .pdf over one merely named url. Set urlField if you would rather be explicit.

Useful options: firstPage and lastPage to read only the front of long reports, maxCharsPerPdf to cap enormous documents, includePageText for per-page text you can cite a page number from, keep: "problems" to find every broken link in a large list, and keep: "needs_ocr" to pull out exactly the documents that need scanning elsewhere.

What it costs

$0.01 per document that returned text, $0.002 per scanned document reported as needing OCR, $0.01 per export file, $0.02 per webhook delivery. Bronze, Silver and Gold Store discounts apply from day one.

  • 500 invoices a month: $5.00
  • 2,000 documents a month with 10% scans: $18.40
  • A nightly run over 50 new filings: about $15 a month

A run costs nothing for the documents it could not read.

Exports and pipelines

  • exportFormats writes a real downloadable CSV or Excel file. Tables and per-page text are JSON-encoded into single cells so they fit a spreadsheet.
  • outputDatasetName appends every run into one named dataset, building a growing document archive. Not charged again.
  • webhookUrl POSTs the run summary to Slack, Zapier, Make, n8n or your own API the moment the run finishes. Charged only on a confirmed 2xx.

FAQ

Does it work on scanned documents? No, and it tells you which ones they are rather than returning empty text and letting you assume it worked. See the limit above.

Will it read a password-protected PDF? No. Those come back as encrypted and are not charged.

My links point at a landing page, not the file. You will get not_a_pdf with a note saying the URL returned HTML. The row is not charged. Google Drive share links are converted to direct downloads automatically.

Can I feed this straight into an LLM? Yes, and textFormat: markdown is the shape to use: text plus proper Markdown tables. Pair it with Dataset AI Enrich to turn the extracted text into typed columns with one plain-English instruction.

Does filtering make a run cheaper? No. The document has to be fetched and read before we know whether it matches, so keep changes what is written, not what is charged. The input schema says so too.

Is my data safe? The Actor reads the documents you point it at and writes the results to your own dataset. Nothing is stored anywhere else. If you set a webhook, the run summary goes to the URL you chose and nowhere else.

The rest of the toolkit

Nine sibling Actors that fit together as a pipeline:

Notes

Text extraction uses unpdf (MIT), a wrapper around Mozilla's pdf.js (Apache-2.0). No browser, no OCR service, no API key, and no third-party service sees your documents.

If this saved you a manual copy-and-paste out of a pile of PDFs, a review on the Apify Store helps a lot. Found a document it read badly? Open an issue with the link and I will look at it.