PDF to Text, Markdown & Tables (CSV) avatar

PDF to Text, Markdown & Tables (CSV)

Pricing

from $1.00 / 1,000 pdf processeds

Go to Apify Store
PDF to Text, Markdown & Tables (CSV)

PDF to Text, Markdown & Tables (CSV)

Extract text, Markdown and tables as CSV from PDF URLs, with optional RAG chunks that keep page numbers. $1 per 1,000 PDFs plus $0.20 per 1,000 pages.

Pricing

from $1.00 / 1,000 pdf processeds

Rating

0.0

(0)

Developer

Matias Isnard

Matias Isnard

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 days ago

Last modified

Security

Limited permissions

Share

Extract text, Markdown and every table as a downloadable CSV from PDF URLs or your own files, with optional RAG chunks that keep the first and last page of each chunk. Scanned pages are flagged with needsOcr instead of coming back silently empty. $1 per 1,000 PDFs plus $0.20 per 1,000 pages, and files that fail are free.

Give it PDF URLs, or a key-value store with your own PDF files. For each PDF you get one dataset item with:

  • the plain text of the whole file and of each page
  • Markdown, with headings guessed from font size and tables as Markdown tables
  • every table found, as a CSV file (with a URL you can download without a token) and as JSON rows
  • metadata: title, author, subject, creator, producer, created and modified dates, page count
  • optional chunks for RAG, with an approximate token count and the first and last page of each chunk

Scanned PDFs (pages that are only images, with no text layer) are flagged with needsOcr: true and the list of those pages, instead of silently returning empty text. The Actor does not run OCR.

A PDF that cannot be downloaded or read gets one item with status: "error" and a readable message, and the run goes on with the next one.

Input

{
"pdfUrls": [
"https://arxiv.org/pdf/1706.03762",
"https://www.irs.gov/pub/irs-pdf/fw9.pdf"
],
"keyValueStoreId": "",
"outputText": true,
"outputMarkdown": true,
"outputTables": true,
"outputChunks": true,
"chunkSize": 500,
"chunkOverlap": 50,
"maxPagesPerPdf": 0,
"password": "",
"maxFileSizeMb": 50,
"downloadTimeoutSecs": 120
}
  • pdfUrls: direct links to PDF files, one per line. Duplicates are processed once. Lines that are not http(s) URLs are skipped with a warning.
  • keyValueStoreId: a key-value store from your account with your own files. See "Your own files" below.
  • outputText (default true): text with the whole document and pages with the text of each page.
  • outputMarkdown (default true): markdown, one block per page, each starting with <!-- page N -->.
  • outputTables (default true): tables, and one CSV file per table in the run's key-value store.
  • outputChunks (default false): chunks for RAG.
  • chunkSize (default 500) and chunkOverlap (default 50): in tokens, counted as characters divided by 4. That is close to what OpenAI and similar tokenizers give for English text, but it is an estimate. Chunks are cut at a space or line break, never inside a word. The overlap is capped at half the chunk size.
  • maxPagesPerPdf (default 0, meaning all): only the first N pages are processed and charged. pageCount still shows the real number of pages.
  • password: for encrypted PDFs. The same password is tried on every file. It is a secret field, stored encrypted and not shown in the run input.
  • maxFileSizeMb (default 50): bigger files get an error item and are not charged.
  • downloadTimeoutSecs (default 120): maximum time to download one file.

Your own files

  1. In Apify Console go to Storage > Key-value stores and create a store (or use an existing one).
  2. Upload your PDFs as records. Give each key a .pdf ending, for example invoice-2026-01.pdf, or set the content type to application/pdf. From the command line: apify key-value-stores set-value <storeId> report.pdf --content-type application/pdf < report.pdf. With the API: PUT https://api.apify.com/v2/key-value-stores/<storeId>/records/report.pdf with header Content-Type: application/pdf and the file as the body.
  3. In the Actor input, pick the store in "Key-value store with your PDFs".

Every .pdf record of the store is processed, together with the URLs in pdfUrls. The Actor gets read access to that store only, nothing else in your account. The url of these items is the record's API URL.

You can also put a record URL like https://api.apify.com/v2/key-value-stores/<storeId>/records/<key> in pdfUrls. That works when the store is picked in the field above, or when the store's General access is set to "Anyone with link can read".

Output

One dataset item per PDF. A real example from a test run, shortened (more in ejemplos/salida.json in the source):

{
"url": "https://slicedinvoices.com/pdf/wordpress-pdf-invoice-plugin-sample.pdf",
"status": "ok",
"processedAt": "2026-10-30T04:21:18+00:00",
"fileSizeBytes": 43627,
"pageCount": 1,
"pagesProcessed": 1,
"needsOcr": false,
"pagesNeedingOcr": [],
"metadata": {
"title": "PDF Invoice Example",
"author": "Sliced Invoices",
"subject": "Example PDF invoice that was created using the Sli",
"creator": "Sliced Invoices",
"producer": "Mac OS X 10.10.4 Quartz PDFContext",
"created": "2016-02-04T00:05:02+00:00",
"modified": "2016-02-04T00:05:02+00:00"
},
"error": null,
"text": "Invoice\nInvoice Number INV-3337\nFrom:\nDEMO - Sliced Invoices Order Number 12345\n...",
"pages": [{ "page": 1, "text": "Invoice\nInvoice Number INV-3337\n...", "chars": 567 }],
"markdown": "<!-- page 1 -->\n\n# Invoice\n\nPayment is due within 30 days from date of invoice. ...",
"tables": [
{
"page": 1,
"index": 2,
"rowCount": 5,
"columnCount": 2,
"csvKey": "table-006-p1-2.csv",
"csvUrl": "https://api.apify.com/v2/key-value-stores/6bgsR9UkDRGs36GCl/records/table-006-p1-2.csv?signature=8Lcot9TKBOXSWvTXihSb",
"rows": [["Invoice Number", "INV-3337"], ["Order Number", "12345"], ["Invoice Date", "January 25, 2016"], ["Due Date", "January 31, 2016"], ["Total Due", "$93.50"]]
}
],
"chunks": [
{ "index": 0, "text": "Invoice\nInvoice Number INV-3337\n...", "pageStart": 1, "pageEnd": 1, "approxTokens": 170 }
]
}
  • status: ok or error. On error only url, status, processedAt and error are set, for example "HTTP 404 Not Found", "Got an HTML page (content type text/html), not a PDF", "PDF is password protected, set the password input" or "File is larger than the 50 MB limit".
  • needsOcr: true when at least half of the processed pages are images without a text layer. pagesNeedingOcr lists those pages, also when the rest of the file has text. Send those files to an OCR tool.
  • metadata: dates are converted to ISO 8601. Fields the file does not have are null.
  • tables[].csvUrl: a signed link that works without an Apify token. The CSV files are in the run's default key-value store, named table-<file number>-p<page>-<table number>.csv, and they are kept as long as that store (the data retention of your plan).
  • pageErrors: only present when some pages could not be read. The other pages are still returned.
  • fullResultUrl: only present when the item would be bigger than 8 MB (Apify's limit for one dataset item is 9 MB). Then text, pages, markdown, chunks and the table rows are left out of the dataset and the full item is saved as JSON in the key-value store, at this link.

The "Overview" view of the dataset shows URL, status, pages, needs OCR, title, author, created date and error.

How it works

Each file is downloaded once with a plain GET, with the size limit and timeout from the input. The file must start with %PDF-, otherwise it is reported as not a PDF. Requests identify themselves with the User-Agent Mozilla/5.0 (compatible; ApifyPdfExtractor/1.0; +https://apify.com/isnard/pdf-text-tables-extractor). Files are processed one after the other.

Text and tables are extracted with pdfplumber, which is built on pdfminer.six. Both are MIT licensed. PyMuPDF is faster and its Markdown is better, but it is AGPL, which would require publishing the source of a hosted service built on it, so it is not used.

  • A page counts as having no text layer when it has fewer than 20 non-space characters and contains at least one image.
  • Tables are found from the lines and cell borders drawn in the PDF. A table needs at least two rows and two columns.
  • In the Markdown, text inside a table is written only once, as the table. A line is a heading when its font is at least 1.25 times the size of the page's body text (##) or at least 1.6 times (#).

Pricing

Pay per event:

  • pdf-processed: each PDF that was downloaded and read, when at least one page could be read.
  • page-processed: each page extracted, up to maxPagesPerPdf. Pages listed in pageErrors are not charged.

Error items (404, not a PDF, too big, wrong password, no readable page, private or local address) are not charged. You can cap the cost of a run with the "Maximum cost per run" setting. When the cap is reached the Actor stops after the current file.

Limitations

  • No OCR. Scanned pages are flagged, not read.
  • Tables without drawn borders (columns aligned only by spaces) are usually not detected as tables. Their text is still in text and markdown.
  • Layout boxes, for example the frame around an invoice, can come out as a table, and a table inside another one appears twice.
  • Multi-column layouts can mix columns in text. Rotated text and some form fields come out in odd order or reversed.
  • Some PDFs space out every letter (0 3 / 2 5), and the text comes out like that.
  • Tokens are an estimate (characters divided by 4), not a real tokenizer.
  • Only files reachable with a plain GET. No cookies, login or custom headers. Sites that block automated requests answer 403, which is reported as an error.
  • Very large or complex files (thousands of pages, heavy vector drawings) are slow, around 1 to 2 seconds per page in the worst case seen in testing. Use maxPagesPerPdf to limit them. Each PDF gets at most 300 seconds: pages after that are left out, not charged, and the item gets a warning.

Run locally

python -m venv .venv
.venv/Scripts/pip install -r requirements.txt # on Linux/macOS: .venv/bin/pip
# put input in storage/key_value_stores/default/INPUT.json
.venv/Scripts/python -m my_actor
python -m unittest discover -s tests -t .

The test fixtures in tests/fixtures are written by tests/make_fixtures.py without any PDF library.

Author: Matias Isnard.

FAQ

Does it read scanned PDFs? No OCR. Scanned pages are listed in pagesNeedingOcr so you can send them to an OCR tool.

Which tables are detected? Tables with drawn lines or cell borders. Columns aligned only with spaces come out as text.

What if a PDF fails? A 404, a non-PDF, an oversized file or a wrong password gives an error item that is not charged.

Size limits? 50 MB per file by default (maxFileSizeMb). maxPagesPerPdf caps pages processed and charged.