PDF Text Extractor — PDF to Text & Markdown API | $3/1k PDFs
Pricing
from $2.40 / 1,000 pdf processeds
PDF Text Extractor — PDF to Text & Markdown API | $3/1k PDFs
Extract text and Markdown from PDF URLs in bulk, with document info (title, dates, page count, producer) and per-page text. Pure JavaScript, no OCR, no API key. Broken links, non-PDFs and scanned files are free rows. $3 per 1,000 PDFs.
Pricing
from $2.40 / 1,000 pdf processeds
Rating
0.0
(0)
Developer
Raffy
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
What does PDF Text Extractor do?
PDF Text Extractor turns PDF files into text and Markdown. Paste a list of PDF links (reports, papers, contracts, manuals, invoices, government forms), upload files, or send them as base64, and get back one clean row per PDF with the full text, an LLM-ready Markdown version and the document's own info: title, page count, creation date, producer software and more. For RAG, turn on RAG chunks and get token-sized chunks with page numbers, section headings and overlap, ready for a vector database. Password-protected PDFs open with your password, and text tables come back as rows of cells and Markdown tables. It works as a PDF to text API and PDF to Markdown converter you can call from code, a schedule or an AI agent: no API key, no browser, nothing to install.
Typical use cases:
- RAG and LLM pipelines: get ready-made chunks (
text,pageStart,pageEnd,section,tokenCount,chunkId) for Pinecone, Qdrant, Weaviate, pgvector, LangChain or LlamaIndex, or feed whole-document Markdown into your own chunker. - Document search and archiving: index the text of hundreds of PDFs from your website, data room or document portal.
- Research and analysis: pull the text of papers, filings, standards and policy documents for summarising or classification.
- Data entry and compliance: read the text of forms and contracts, and check document info (dates, producer, encryption, form fields) in bulk.
- AI agents: a cheap PDF reader tool with plain inputs and self-describing rows.
Why use this PDF text extractor?
- Bulk by default. Hundreds of PDF URLs in one run, downloaded in parallel; every PDF is one row.
- Text and Markdown in one pass. Plain text keeps the PDF's lines; Markdown rebuilds headings from font sizes, bullet and numbered lists and wrapped paragraphs, and removes page numbers and the running headers and footers repeated across pages.
- RAG chunks with citations. One row per chunk of about
chunkSizeTokenstokens (default 500), split at paragraphs and sentences, with a configurable overlap, the page numbers each chunk comes from and the heading it sits under. A PDF of up to 50 chunks costs the same as in the default mode. - Files, not only links. Upload PDFs in Apify Console, point at records in your key-value stores (read with your own run token, so private stores work), or send base64 from your code.
- Password-protected PDFs. Give the password once (stored encrypted, never logged or written out); a wrong password is a free
wrong_passwordrow. - Text tables. With
extractTables, tables in the text layer become atablesarray (rows of cells) and Markdown tables, so numbers stay in their columns. - Per-page text when you need it. Turn on
includePageTextfor apagesarray (still one result per PDF), orsplitByPagefor one row per page. - Honest results, honest billing. Every row has a
status(ok,not_found,error) and, when something went wrong, anerrorType. A broken link, a web page instead of a PDF, a password-protected, damaged, oversized or scanned PDF is one free row, and the rest of the batch still runs. Only PDFs whose text was extracted are charged. - Simple, flat price. $3 per 1,000 PDFs (up to 100 pages each by default), whatever the page count.
- Share links just work. Google Drive, Dropbox and GitHub file links and arXiv abstract pages are turned into their download links automatically.
What data can PDF Text Extractor extract?
One row per PDF (default):
| Field | Type | Description |
|---|---|---|
fileName | string | File name from the server's Content-Disposition header or the URL |
title | string | Title from the PDF's document info (or XMP metadata) |
pageCount | integer | Total pages in the PDF |
pagesExtracted | integer | Pages read for this row (after pageRange and maxPages) |
extractedPages | string | Which pages were read, e.g. 1-5, 8 |
pageNumber | integer | Page number, only on rows written with splitByPage |
charCount | integer | Characters of extracted text |
wordCount | integer | Words of extracted text |
needsOcr | boolean | true when the text layer is missing (scanned or image-only pages) |
pagesWithoutText | integer | Pages read that had no text layer |
truncated | boolean | true when maxPages or the timeout stopped the PDF early; warning says how far it got |
text | string | Plain text, line by line as in the PDF, blank lines between blocks and pages |
markdown | string | Markdown with # headings, - and 1. lists and reflowed paragraphs |
subject, keywords | string | The PDF's document info fields (the author field is never returned: it usually holds a person's name) |
creator, producer | string | The application that made the document and the PDF |
creationDate, modificationDate | string | ISO 8601 dates in UTC |
pdfVersion, language | string | e.g. 1.7, en-US |
hasForms, isEncrypted | boolean | Fillable form fields present; permissions-only encryption (it still opens) |
fileSizeBytes, contentType | integer, string | The downloaded file's size and the Content-Type the host sent |
pages | array | With includePageText: [{ pageNumber, text, markdown, charCount, hasText }] |
tables, tableCount | array, integer | With extractTables: [{ pageNumber, rowCount, columnCount, rows: [["Region", "Q1"], ["North", "1,200"]] }] |
source | string | base64 or keyValueStore for files that did not come from a URL |
inputUrl, finalUrl | string | The link as you typed it (when rewritten) and where redirects ended |
url | string | The PDF URL that was downloaded |
status | string | ok, not_found or error (see below) |
errorType | string | Machine-readable reason on rows that are not ok |
error | string | Human-readable reason on rows that are not ok |
scrapedAt | string | ISO 8601 time the PDF was processed |
RAG chunk rows (ragChunks), one per chunk:
| Field | Type | Description |
|---|---|---|
text | string | The chunk: Markdown (headings, lists, tables) unless Output format is plain text (chunkFormat says which) |
chunkId | string | Stable id: the same PDF URL gives the same ids on every run (<hash>-<index>) |
chunkIndex, chunkCount | integer | Position of the chunk (from 0) and the number of chunks in the PDF |
pageStart, pageEnd, pageNumbers | integer, integer, array | The page(s) the chunk's text comes from, for citations |
section | string | The nearest heading above the chunk |
tokenCount | integer | Estimated tokens (about 4 characters of English per token, 1 per CJK character) |
fileName, title, pageCount, url | The PDF the chunk belongs to (on every chunk); the other document info fields are on chunk 0 |
Result status (tri-state output)
status | errorType | Meaning | Charged? |
|---|---|---|---|
ok | - | Text was extracted | Yes, one result (RAG chunks: one per started 50 chunks of a PDF) |
not_found | http_404, http_410 | No file at the URL | No |
not_found | no_text | The PDF (or page) has no text layer: scanned or image-only; needsOcr is true | No |
not_found | no_pages_selected | pageRange selects no page of this PDF | No |
error | not_pdf | The URL returned a web page or another file type, not a PDF | No |
error | too_large | Over maxFileSizeMb (checked before downloading when the host sends the size) | No |
error | password_protected | The PDF needs a password to open and none was given | No |
error | wrong_password | The password given does not open the PDF | No |
error | invalid_input | A base64Files / fileUploads entry could not be used | No |
error | malformed | The file is damaged or not a valid PDF | No |
error | timeout | The download or the reading ran out of time | No |
error | blocked, rate_limited, http_error, network, download_failed | The host refused or failed the download (after retries) | No |
error | output_too_large | The text is larger than one dataset row can hold (9 MB); use splitByPage | No |
The run itself still succeeds when some PDFs fail, so one bad link never costs you the rest of the batch.
How to use the PDF to text converter
- Click Try for free and paste your PDF links into PDF URLs, one per line, or upload files under Upload PDFs or key-value store records.
- Pick the Output format: text and Markdown (default), text only, or Markdown only.
- Optional: set a Page range such as
1-10, turn on Include per-page text, One row per page, RAG chunks or Extract tables, or enter a PDF password. - Click Start. A typical PDF takes a second or two to read; the default example (two government PDFs, 34 pages) finishes in under 20 seconds.
- Download the results as JSON, CSV, Excel or HTML, or read them through the Apify API.
From code, call the Actor with the Apify API or the Apify client for JavaScript or Python and pass the same input as JSON.
How much does it cost to extract text from PDFs?
Pay-per-event pricing; Apify platform usage (compute) is included, so you pay only these events:
| Event | Price |
|---|---|
| Actor start | $0.005 per run |
PDF processed (status: ok row) | $0.003 per PDF ($3 per 1,000) |
Examples:
- 1 PDF: $0.005 + $0.003 = $0.008.
- 100 PDFs of any length (up to
maxPages, 100 by default): $0.005 + 100 x $0.003 = $0.305. - 1,000 PDFs: $3.005.
- With One row per page, every page with text is one result: a 20-page PDF costs 20 x $0.003 = $0.06. For per-page text at the per-PDF price, use Include per-page text instead.
- With RAG chunks, a PDF is one result per started 50 chunks: a 30-page report in 500-token chunks (about 40 chunks) is $0.003, a 100-page manual (about 140 chunks) is 3 x $0.003 = $0.009. That is $0.06 per 1,000 chunks at most.
- Uploaded files, key-value store records and base64 files cost the same as URLs.
Broken links, non-PDF files, password-protected, damaged or oversized files and scanned PDFs without a text layer are free rows. Cap spending with Maximum rows and the run's Max total charge.
Input
| Field | Type | Default | Description |
|---|---|---|---|
startUrls | array | two example PDFs | PDF links, one per line; share links are rewritten to download links |
fileUploads | array | none | Uploaded files, key-value store record URLs, or storeId/key; read with your run's token |
base64Files | array | none | ["JVBERi0..."] or [{ "fileName": "a.pdf", "base64": "JVBERi0...", "password": "..." }]; a data URL works too |
password | string (secret) | none | Opens password-protected PDFs; never logged or output |
outputFormat | string | both | both, text or markdown |
splitByPage | boolean | false | One row per page (each page with text is one result) |
includePageText | boolean | false | Add a pages array with every page's text to each PDF row |
ragChunks | boolean | false | One row per token-sized chunk (replaces splitByPage) |
chunkSizeTokens | integer | 500 | Chunk size in estimated tokens, 100 to 2,000 |
chunkOverlapTokens | integer | 50 | Tokens repeated from the previous chunk, 0 to 500 (at most half a chunk) |
extractTables | boolean | false | Text tables as a tables array and Markdown tables |
pageRange | string | all pages | e.g. 1-5, 8, 12- (12- = to the end, -3 = first three) |
maxPages | integer | 100 | Pages read per PDF, up to 1,000 |
includeMetadata | boolean | true | Add the PDF's document info fields |
maxFileSizeMb | integer | 50 | Skip larger files, up to 100 MB |
timeoutSecs | integer | 120 | Time allowed per PDF for downloading, and again for reading |
maxItems | integer | 1000 | Safety cap on rows written (with RAG chunks: on PDFs) |
proxyConfiguration | object | off | Apify Proxy, only for hosts that block direct downloads |
Example input (RAG chunks, with a private file from a key-value store and one sent as base64):
{"startUrls": ["https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf"],"fileUploads": ["<storeId>/annual-report.pdf"],"base64Files": [{ "fileName": "contract.pdf", "base64": "JVBERi0xLjcK...", "password": "your-password" }],"ragChunks": true,"chunkSizeTokens": 500,"chunkOverlapTokens": 50,"extractTables": true}
Example input (one row per PDF):
{"startUrls": ["https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf","https://www.irs.gov/pub/irs-pdf/p1.pdf"],"outputFormat": "both","pageRange": "1-20","includePageText": true}
Output
Real rows from the default run (2026-10-16), with text and markdown shortened:
[{"url": "https://www.irs.gov/pub/irs-pdf/p1.pdf","status": "ok","fileName": "p1.pdf","title": "Publication 1 (Rev. 9-2017)","pageCount": 2,"pagesExtracted": 2,"extractedPages": "1-2","charCount": 11376,"wordCount": 1881,"needsOcr": false,"pagesWithoutText": 0,"truncated": false,"text": "Your Rights\nas a Taxpayer\n\nThe IRS Mission\n\nThe Taxpayer Bill of Rights\n\nPublication 1\n\nThis publication explains your rights as a taxpayer and the processes for examination, appeal, collection, and refunds.\nAlso available in Spanish. ...","markdown": "# Your Rights as a Taxpayer\n\n## The IRS Mission\n\n# The Taxpayer Bill of Rights\n\n## Publication 1\n\nThis publication explains your rights as a taxpayer and the processes for examination, appeal, collection, and refunds. Also available in Spanish. ...\n\n## 1. The Right to Be Informed\n\nTaxpayers have the right to know what they need to do to comply with the tax laws. ...","subject": "Your Rights as a Taxpayer","creator": "Adobe Illustrator CS6 (Windows)","producer": "Adobe PDF library 10.01","creationDate": "2017-10-31T16:08:08.000Z","modificationDate": "2017-10-31T16:16:24.000Z","pdfVersion": "1.4","hasForms": false,"isEncrypted": false,"fileSizeBytes": 324302,"contentType": "application/pdf","scrapedAt": "2026-10-15T23:47:42.646Z"},{"url": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf","status": "ok","fileName": "NIST.CSWP.29.pdf","title": "The NIST Cybersecurity Framework (CSF) 2.0","pageCount": 32,"pagesExtracted": 32,"extractedPages": "1-32","charCount": 68724,"wordCount": 9421,"needsOcr": false,"text": "National Institute of Standards and Technology\n\nThis publication is available free of charge from: https://doi.org/10.6028/NIST.CSWP.29\n\nFebruary 26, 2024\n\nThe NIST Cybersecurity\nFramework (CSF) 2.0 ...","markdown": "### National Institute of Standards and Technology\n\nThis publication is available free of charge from: https://doi.org/10.6028/NIST.CSWP.29\n\nFebruary 26, 2024\n\n# The NIST Cybersecurity Framework (CSF) 2.0 ...","keywords": "cybersecurity; Cybersecurity Framework (CSF); cybersecurity risk governance; cybersecurity risk management; enterprise risk management; Profiles; Tiers","creator": "Acrobat PDFMaker 23 for Word","producer": "Adobe PDF Library 23.8.53","creationDate": "2024-03-06T20:46:45.000Z","pdfVersion": "1.6","language": "EN-US","fileSizeBytes": 1518858,"scrapedAt": "2026-10-15T23:47:43.244Z"}]
A link that is not a PDF comes back like this, free:
{"url": "https://example.org/annual-report","status": "error","errorType": "not_pdf","error": "The URL returned a web page (text/html), not a PDF file. Use the direct link to the .pdf file; many sites show a viewer or download page first. Not charged.","contentType": "text/html"}
Tips
- Use Markdown for LLMs and RAG: headings and lists survive, and page numbers and the running headers and footers repeated at the top or bottom of the pages are removed.
- Long PDFs: raise Max pages per PDF (up to 1,000) or read the part you need with Page range.
- Need page numbers for citations? Include per-page text keeps one result per PDF and adds every page in
pages. - Filter failures by
errorTypeinstead of parsingerror;needsOcr: truemarks scanned documents to send to an OCR tool.
Limitations
- No OCR. Scanned or image-only PDFs have no text layer; they come back as free
not_foundrows withneedsOcr: true. PDFs that already carry an OCR text layer are read normally. - Tables: text tables only.
extractTablesfinds tables whose cells sit in columns separated by white space in the text layer, row by row. Tables that are images, cells drawn column by column, merged cells and very irregular layouts stay as plain text lines; a two-column page whose columns share baselines may come out as a two-column table. Check thetablesyou rely on. - Token counts are estimates.
tokenCountuses about 4 characters per token for English and 1 per CJK character, not a model tokenizer; keep a margin under your embedding model's limit. - Running headers and footers are recognised when the same text (up to three lines) repeats at the top or bottom of at least 3 pages and 40% of the pages read; on shorter documents only page numbers are removed. The plain
textfield always keeps every line. - Headings come from font size. Bold text at body size is not detected as a heading, and images, charts and equations are not converted.
- Reading order follows the PDF. Most PDFs (Word, LaTeX, InDesign exports) come out column by column; a PDF whose internal order is scrambled comes out scrambled.
- Hyphenation. For TeX-made PDFs a hyphen at a line end is removed when the word continues on the next line; for other producers it is kept (
high-level), which occasionally leaves a split word with a hyphen. - Passwords. PDFs that need a password to open are read when you give the password (
password, or per file inbase64Files); without it they are freepassword_protectedrows. Permissions-only encryption is fine. The Actor never guesses or cracks passwords. - Base64 size. Base64 files travel inside the run input, which Apify limits in size; send large files as key-value store records instead.
- Size and length. Up to 100 MB and 1,000 pages per PDF; one dataset row holds at most about 9 MB of text (use
splitByPagefor huge documents). - The host decides. A file behind a login, a cookie wall or an anti-bot challenge cannot be downloaded; you get a free
blockedrow. Try Apify Proxy for hosts that block direct downloads.
FAQ
Is there a PDF to text API I can call from my code?
Yes. Every Apify Actor is an API: start a run with your PDF links and read the dataset as JSON, CSV or Excel, or call it synchronously and get the rows in the response. The input and output are the tables above.
Can it convert PDF to Markdown for ChatGPT, Claude or a RAG pipeline?
Yes, that is what the markdown field is for: headings, lists and paragraphs are rebuilt and page furniture (page numbers, and running headers and footers repeated across pages) is removed, so the text chunks cleanly. Or let the Actor chunk it: RAG chunks writes one row per chunk with the page numbers, the section heading, an estimated token count and a stable chunkId, ready to embed.
Can I process PDFs that are not online?
Yes. Upload them in Apify Console (Upload PDFs or key-value store records), store them in a key-value store and pass storeId/key, or send them as base64 in base64Files from your code. Each file is billed like a URL.
Does it read scanned PDFs?
No. It reads the text layer that digital PDFs carry. A scanned PDF without one is reported as not_found with needsOcr: true and is not charged, so you can route it to an OCR tool.
Why is a PDF that opens in my browser reported as not_pdf or blocked?
Some sites link to a viewer or download page rather than the file, or only serve the file after a login or a challenge. Use the direct link to the .pdf file. For Google Drive, Dropbox and GitHub, paste the normal share link; it is converted for you.
Can I use it from an AI agent or MCP client?
Yes. The input has safe defaults, every row explains itself with status and errorType, and failed documents are free, so an agent can pass a list of links and read the result without special handling.
Is my document data stored or shared?
The Actor only downloads the URLs you give it, reads the key-value store records you point it at (with your own run's token) and the base64 files in your input, and writes the results to your own run's dataset in your Apify account. It does not send the documents anywhere else. A PDF password is a secret input: Apify stores it encrypted, and the Actor never logs it or writes it to the dataset.
Legal and data-protection notice
This Actor processes only the documents you provide (links, uploaded files, key-value store records or base64) and returns what those documents contain, plus the technical document info stored in the PDF (title, dates, producer software). The PDF's author field is deliberately not returned. It does not log in, does not guess or crack passwords (it uses only the password you give it for your own documents), does not get around access controls and does not collect data from any website on its own. Documents can contain personal data. Personal data is protected by the GDPR in the European Union and by other regulations around the world. You should not process personal data unless you have a legitimate reason to do so, and you are responsible for having the right to process the documents you submit and for complying with applicable law.
This Actor is an independent tool. PDF is an open standard (ISO 32000); text extraction uses the open-source pdf.js library. All trademarks belong to their respective owners.