PDF Text Extractor: OCR, Markdown, Page Checks
Pricing
from $1.93 / 1,000 document returneds
PDF Text Extractor: OCR, Markdown, Page Checks
Read your PDFs and get the text plus a verdict on every page — the file's own layer, OCR for scans at no per-page charge, or a row saying why it couldn't be read. A wrong layer is flagged, not passed off as clean. Full text or Markdown, page ranges, locked files, uploads. Failed files cost nothing.
Pricing
from $1.93 / 1,000 document returneds
Rating
0.0
(0)
Developer
Pradio Actors
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
What does PDF Text Extractor do?
PDF Text Extractor reads your PDFs and gives you back one row per file: the text, plus a verdict on every page. Give it links, an upload, files sent inline or a saved table of links, then press Start. If a page is a scan, we read it with OCR — that's inside the document price. If a page's own text layer fails the check, we flag it instead of handing it to you as clean text.
$3.25 per 1,000 documents, and a miss costs nothing. The price covers the first 50 pages of a file; after that it's $0.10 per 1,000 pages. The optional page check adds $8 per 1,000 pages checked.
Read the text of a public PDF:
{"urls": ["https://ntrs.nasa.gov/api/citations/20160001713/downloads/20160001713.pdf"]}
In a 52-file run it read 45 of them, about 26 seconds each at the default memory. The other 7 came back as rows saying exactly why they failed.
Who uses PDF Text Extractor
| Buyer | What you run it for |
|---|---|
| Search and AI pipeline builders | Checked text into indexes and embeddings |
| Compliance and legal teams | A check on every document before anyone reads it |
| Civic-data projects | Scanned or broken files out of public PDF batches |
| Document-ingest operators | Which batch files read clean and which need re-sourcing |
Features
- A verdict on every page it can judge. The file's own layer, OCR, what made a page unreadable — or an honest
unverifiedwhen the checks can't say. - OCR inside the price. A page with no text layer is rendered and read inside the document's charge.
- The page check. Turn it on and we render every page and compare its text layer with what the page actually shows. If they disagree we flag it and use the rendered text.
- Four ways to send files. Links, an upload, base64 inline, or a saved dataset.
- Markdown, chunks, cleanup. Markdown on request, page-tagged chunks, repeated headers and footers dropped.
- Tables, images, phrases. Detected tables rebuilt, embedded images saved, up to 20 phrases screened.
- Tuned OCR. Language, resolution, rotation retries, layout, confidence floor, scan cleanup.
- Repeat runs skip unchanged files. A file already returned under the same options is skipped.
- Caps you control. How many files a run takes, how many pages each contributes, how many get OCR, how big a file can be, how long a download may take.
What you can count on
- You only pay when a file is actually read: if it can't be answered you get a free row naming the file and why.
- A row is charged only after it lands in your dataset.
- If nothing comes back, you get one row saying so.
- Set a spending limit and the run stops cleanly; the last row carries the counts.
- Every run saves a summary of what it read, returned and charged.
- If the run itself breaks, it fails and says why in the log.
- If a file doesn't carry something a field asks for, that field comes back empty.
Why this one
| PDF Text Extractor | Most-used alternative | |
|---|---|---|
| Rows on the same document | 1 | 1 |
| Price per 1,000 documents | $3.25 | $5.00 |
| Start charge per run | $0.00005 | $0.005 |
| A scanned page read by OCR | inside the document price | $0.015 a page |
| A verdict on every page | Yes | No |
Run on the govinfo.gov document in the example list, measured 2026-09-22. The per-page figure is the alternative's own pricing.
What data does PDF Text Extractor return?
A real row the example link returned, cut to the fields checked first:
{"url": "https://ntrs.nasa.gov/api/citations/20160001713/downloads/20160001713.pdf","file_name": "20160001713.pdf","row_type": "ROW","status": "agree","recommended_action": "Use the text as it is.","page_count": 14,"pages_extracted": 14,"word_count": 895,"pdf_type": "text_based","verdict_counts": {"agree": 14, "suspect_layer": 0, "wrong_layer": 0, "hidden_content": 0, "no_layer": 0, "unreadable": 0},"extra_pages": 0,"text_preview": "www.nasa.gov \nNational Aeronautics and Space Administration \nRelative Terrain Imaging ..."}
On the 52-document run, 32 files read fully clean, 13 carried OCR-read pages, 16 a page that gave nothing.
Every field a row can carry:
| Field | What it holds |
|---|---|
url | The link this row answers; null on an inline file or repeat |
file_name | The link's last part, or the name you gave an inline file |
download_url | The link the file was actually fetched from when it differs from the one given: a share link's download URL or a redirect's target; null on a direct link |
entry | The input line the row answers; on repeat rows, where url is null |
status | agree or the dominant page flag, unverified when its pages could not be judged; the failure word on a miss row |
recommended_action | One plain sentence on what to do with the document; null on a miss |
miss_code | The cause code on a miss row, each one explained in Miss codes below; null on a document row |
page_count | Pages the PDF declares |
word_count | Words in the delivered text |
pdf_type | text_based, scanned, mixed or unreadable |
pages_extracted | Pages that delivered text, by layer or OCR |
title | The PDF's metadata title; null when it declares none |
created | The PDF's creation date, ISO-8601; null when it carries none |
modified | The PDF's last-modified date, ISO-8601; null when it carries none |
file_size_bytes | The file's size in bytes |
document_hash | SHA-256 of the bytes read; null when no file was read |
char_count | Characters in the delivered text, page joins included |
text | The whole document text, a blank line between pages; null when Markdown alone is wanted |
markdown | The document as basic Markdown; when the format asks |
chunks | Page-tagged text pieces for an index; under Chunk size |
page_range | The range read, normalised; null on a whole document |
must_contain_results | Each required phrase: found, its pages, the first matching line |
all_phrases_found | True when every required phrase was found |
row_type | ROW, ITEM_STATUS, NO_TEXT_FOUND or STOPPED_EARLY |
reason | Status rows: why the entry was not answered, the run was empty, or it stopped early |
rowsFetched | Status rows: rows fetched before de-duplication and the cap |
rowsReturned | Status rows: document rows in the dataset, 0 on an empty result |
rowsRemaining | Status rows: fetched rows the run did not return |
capped_entries | Status rows: entries Maximum items held back, none read or billed |
dropped_entries | Status rows: entries the keep filter dropped, on the kept-out row |
page_verdicts | Per page: verdict — or null with unverified naming why the checks couldn't judge — which side delivered, the checks' scores, the text |
per_page_method | Per page: embedded layer, OCR, or neither produced text |
layer_ocr_agreement | Mean layer-versus-OCR agreement; under the page check |
wrong_layer_pages | Pages whose render showed text the layer lacks; under the page check |
suspect_layer_pages | Pages whose layer failed the statistics check |
ocr_pages | Pages whose text came from OCR |
unreadable_pages | Pages that gave no text from either side |
verified_pages | Pages the render check verified; empty without the page check |
pages_truncated | True when the page cap cut the read short |
low_confidence_pages | OCR pages under your confidence floor |
ocr_capped_pages | Pages still needing OCR after the per-document cap |
verify_inconclusive_pages | Checked pages whose OCR read was too weak to judge the layer |
tables | Rebuilt tables with cells and a rebuilt-or-uncertain mark; when Extract tables is on |
images | Embedded images saved as PNGs: page, link, size; when Extract embedded images is on |
verdict_counts | The tally of page verdicts across the document |
text_preview | The first 200 characters of the delivered text |
metadata | The PDF's document-info dictionary; null when it carries none |
processed_at | When the row was produced, ISO-8601 |
processing_ms | Milliseconds this entry's own read took; null on an entry never reached |
size_trimmed_fields | What was shortened to fit one dataset row; null on almost every row |
text_cleanup | What the clean-up pass removed; when Clean up the text is on |
extra_pages | Extracted pages past the fiftieth; the count extra-page billing reads |
How much does it cost?
$3.25 per 1,000 documents. Each file you get back bills once, $0.00325, which covers its first 50 pages. Past that it's $0.0001 a page. Turn the page check on and each checked page bills $0.008 — and only the pages the row lists as verified bill. Apify adds its own start charge per run, $0.00005 at the default 1024 MB.
| Documents given | Billed rows | You pay | If every file answers |
|---|---|---|---|
| 100 | about 74 | about $0.24 | $0.325 |
| 1,000 | about 742 | about $2.41 | $3.25 |
| 10,000 | about 7,424 | about $24.13 | $32.50 |
The billed share counts all 66 documents the test list names; on the 52-document run the 45 answered files carried 1,307 pages past the fiftieth, another $0.13. On Apify's paid plans the document charge drops: Bronze $0.00275, Silver $0.002, Gold, Platinum and Diamond $0.00193. Failures, duplicates, skipped-unchanged files, capped entries and rows the keep filter drops are free.
How do I use PDF Text Extractor?
- Press Try for free.
- Paste links into PDF links, or upload one PDF with the file picker.
- Press Start; rows land in your dataset as JSON, CSV or Excel.
Worked examples
Use it to fill a spreadsheet with document text:
{"urls": ["https://www.govinfo.gov/content/pkg/FR-2026-09-21/pdf/2026-19336.pdf","https://ntrs.nasa.gov/api/citations/20240009519/downloads/NASA_shielding_SmallSat_collaboration_08062024.pdf"]}
Use it to turn a long report into Markdown for a RAG index:
{"urls": ["https://ntrs.nasa.gov/api/citations/20160001713/downloads/20160001713.pdf"],"outputFormat": "both"}
Use it to check a report's first pages against their render ($0.008 a verified page):
{"urls": ["https://ntrs.nasa.gov/api/citations/20160001713/downloads/20160001713.pdf"],"pageRange": "1-3","verifyPages": true}
Input
| Input | Type | Default | What it does |
|---|---|---|---|
urls | array | 3 example links | PDF links, one per line; Drive and Dropbox shares resolve |
base64Pdfs* | array | none | Files inline: a bare base64 string, or {name, data} plus a password |
pdfFile | file | none | One PDF picked in the Console, or its web URL over the API |
inputDatasetId* | string | none | A saved dataset's id; anyone with it can read it |
urlField* | string | url | The item column each link sits in |
outputFormat | select | text | Whole-document columns: text, markdown or both |
keepRows | select | all | Which results come back: all, answered, flagged or missed |
cleanText | boolean | off | Drop repeated headers and footers, page-number lines, split hyphens |
extractTables | boolean | off | Rebuild detected tables into rows of cells |
extractImages* | boolean | off | Save embedded images as PNGs in the run's store |
skipEmptyPages* | boolean | off | Leave proven-empty pages out of the page lists and counts |
pageRange | string | all pages | Read only these pages: 1-5,8, 10-, -3 for the last three |
mustContain* | array | none | Up to 20 phrases every document must carry |
pdfPassword | string | none | Opens your protected PDFs; an entry's own password wins |
ocrLanguage | select | eng | The language scans are read in; join codes with + |
verifyPages | boolean | off | Render and OCR-check every page, $0.008 a page verified |
pdfTextMode | select | auto | text never OCRs, ocr reads every page by OCR; text is refused under the page check |
autoRotate* | boolean | on | Retry a badly read scan turned 90, 180 and 270 degrees |
dpi* | integer | 144 | Render resolution for OCR, 72 to 400 |
preprocessMode | select | none | A cleanup the render gets before OCR: grayscale, contrast, binarise |
minOcrConfidence* | integer | none | Flag OCR pages under this score, 0 to 100; the words stay, marked |
segmentationMode | select | auto | The page layout OCR assumes; orientation choices detect rotation |
includeWords* | boolean | off | OCR pages list every word with confidence and position |
chunkSize* | integer | none | Split the text into chunks of about this size, 200 to 20,000 |
chunkOverlap* | integer | 200 | Characters shared between chunks, at most half the size |
maxItems* | integer | 100 | The most rows a run returns, billed and unbilled both counting |
maxPagesPerDocument* | integer | 300 | The most pages one document contributes; more marks it truncated |
ocrMaxPagesPerDocument* | integer | none | Cap OCR pages a document; an over-cap page is marked, never skipped |
maxConcurrency* | integer | 5 | Documents read at once, up to five; rows keep input order. A run that renders (verifyPages, OCR modes, extractImages) reads fewer at once, bound by the run's memory allocation |
lowMemory* | boolean | off | One document at a time, so the biggest files fit 1024 MB |
requestTimeoutSecs* | integer | 60 | Seconds one download may take, 5 to 600 |
maxDocumentSizeMb* | integer | 80 | A file over this answers a too-large row, 1 to 250 |
onlyNew* | boolean | off | Skip files an earlier run already returned unchanged under the same reading options |
*Marked inputs are JSON editor or API only; the form hides them.
The flagged choice keeps documents marked suspect, wrong, hidden, unreadable or unverified; a clean scan read by OCR alone stays an answer.
What the default checks cover, and what they don't. Without Verify pages, no page is rendered. The checks read the layer's own statistics. Malformed glyphs mark it suspect_layer. They also read the paint order behind it: text drawn white, invisible, too small to see, off the page or under a picture is in the layer but not on the page a buyer sees. A run of it marks the page hidden_content. The limits: a scan keeps its text in an invisible layer under the page image. Faithful and garbled read the same to statistics, so those pages come back unverified rather than agreed. Pages with no layer at all are read by OCR. Where that read is too weak to tell whether the page held words, the page is unverified, not guessed.
The page check. With Verify pages on, we render every page and read the picture with OCR. If a born-digital text layer disagrees with what the page shows, we flag it and give you the OCR text instead. We can't check the text a scanner wrote into a file. A fresh OCR read is a second transcript, not a witness, so those pages come back verify_inconclusive with no verdict. The same goes for a page whose read was too weak to tell whether it held words. Every inconclusive page is listed in verify_inconclusive_pages and bills nothing. A verified page bills $0.008.
Output
Everything you give it comes back named: a normal row when the file reads, or a row saying why it couldn't. That covers repeats, files it already gave you, bad links, entries past Maximum items or your spending limit, and rows your keep filter drops — each gets its own named row. Rows come back in the order you gave them.
row_type | What it marks |
|---|---|
ROW | A document row |
ITEM_STATUS | An entry the run could not answer; the miss code and reason say why |
NO_TEXT_FOUND | A run with nothing to return |
STOPPED_EARLY | A charge or spending limit ended the run early; the count fields carry the numbers |
A file's status is agree when its pages pass the checks. One page of hidden_content, text the render never shows, marks the whole file at any share. An injected line is enough. Otherwise, if a fifth or more of its judged pages share one flag, that flag becomes the file's status: suspect_layer when the statistics check doesn't like a layer, wrong_layer when the page check disagrees, no_layer when only OCR could read the text, unreadable when the pages gave nothing. unverified means we read the file but couldn't judge its pages, so it is not a claim the text is good. Some carry the text a scanner wrote in. No check can certify it, the page check included. Others are reads too weak to tell. They want a clearer file or a higher ocrDpi. A page your OCR budget stopped wants a bigger one. What re-running does settle is a suspect_layer on a born-digital page: the page check can confirm or clear the flag against the render. When an entry fails, the status names the failure family and the miss code says why: not_found, blocked, bad_url, fetch_failed, timeout, bad_input, failed, aborted. aborted also covers an entry it deliberately didn't read: dropped by your keep filter, already returned unchanged, a repeat of an earlier entry, held back by the item cap, or never reached before your spending limit.
An empty run is an answer too: you get one row saying what you gave it and why nothing came back. When a filtered or stopped run ends, the status rows carry what was fetched, returned and left; a store record lists what a repeat run skipped.
Miss codes
Every miss row names its cause in one code, and every code below says what happened and what to do about it. None of these rows is billed. Where two cases answer with the same code, the row's reason says which one it was.
The link or the file
| Miss code | What it means |
|---|---|
bad_url | The line is blank, or it is not an http(s) link. Give a full link to the file |
credentials_in_url | The link carries a user name or password, so nothing was requested. Put a PDF password in pdfPassword, and give only files that open without a login |
sign_in_required | The link answered with a sign-in page, so the file is not shared publicly. Share it publicly, or send the file itself in pdfFile or base64Pdfs |
http_<status> | The host answered with that status instead of the file. http_404 and http_410 are gone or moved; http_401, http_403 and http_429 are a login, a refusal or a rate limit; another 4xx is the link's own fault; a 5xx is the host's |
not_pdf | What came back is not a PDF. Check the link still points at the file |
corrupt_pdf | The file starts like a PDF but cannot be read as one. Fetch it again, or take another copy |
password_required | The PDF is protected and no password opened it. Set pdfPassword, or a password on the entry itself; if you did send one, the reason says the value was wrong |
extraction_not_permitted | The file's own permissions forbid copying its text. Get the owner password, or a copy you are allowed to read |
too_large | The file is over maxDocumentSizeMb, 80 MB by default. Raise the cap, or send a shorter document |
bad_page_range | The page range is not a range, or it selects no page of that document. Give pages the document has, such as 1-5,8 |
bad_base64 | An inline entry is not readable base64. Send { "name": "...", "data": "<base64>" } |
bad_input | The run refused the input itself, such as two options that cannot hold together. The reason names the option and what to change |
read_error | The PDF opened but could not be read. Fetch it again, or send another copy |
The host or the network
| Miss code | What it means |
|---|---|
dns_failure | The host name does not resolve. Check the link's domain |
connection_refused | The host refused the connection. Check the port, or whether the service is up |
host_unreachable | The host could not be reached at all. Check the network path |
connection_reset | The host closed the connection mid-download. Try again later |
timeout | Nothing arrived for a minute. Raise requestTimeoutSecs, or try again |
tls_error | The host's HTTPS certificate was rejected. Fetch it over http, or fix the certificate |
redirect_loop | The link redirects to itself. Give the final link |
network_error | The download failed for a reason we could not name. Try again |
host_stopped | This host refused the run — a 403, a 429 we did not wait out, or an anti-bot challenge page — and we send it nothing more. The reason says whether your URL was never requested or was answered with a challenge. Come back later, or reach the file another way |
What the run did with your entry
| Miss code | What it means |
|---|---|
duplicate | The same entry, or the same bytes under another name, is in this run twice. The first entry's row answers both |
already_returned | Under onlyNew, an earlier run already returned this file unchanged under the same options. It is not read or billed again |
cap_reached | maxItems held this entry back; capped_entries names every held entry. Raise the cap, or split the batch |
kept_out | Your keep filter dropped every row, so nothing came back. dropped_entries names each entry and the reason says what became of it — raise the cap, widen the filter, or split the batch |
aborted | The run stopped before this entry finished, on your spending limit or otherwise. It was not billed |
What can you do with the data?
Feed a search index text where every page says how it was read — chunks name their pages, so an answer can cite the page it came from. Gate a batch by status and recommended action, or hand a flagged file to the page check when a flag looks wrong to you.
Use PDF Text Extractor with AI agents
One line gives a compatible agent this Actor:
$claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/pdf-text-verifier"
Personal data
It reads your files and stops there: the text comes back to you, we keep none of it after the run, and nothing is inferred from it. The only thing kept between runs is a fingerprint of each file it already gave you plus your options — a hash, not the content — in a store in your own account that you can delete. A document can carry personal data as written; you are its controller.
Release notes
- 2026-10-10: the default checks now see the paint order, not just the text. A page hiding words the render never shows (white, invisible, tiny, off-page or under a picture) is
hidden_contentwithout Verify pages on, and one such page marks the document. A scan's invisible transcript layer, which statistics cannot tell faithful from garbled, no longer answersagree; it carries a null verdict flaggedunverified, and a document the checks cannot judge answersunverifiedrather than claiming the text is clean. Blank pages now agree with the empty page they render.suspect_layerneeds real corruption (malformed glyphs), so figure-heavy and acronym-dense pages stop flagging. Verify pages learned the same honesty. It can't judge a scan's own transcript either, so it marks those pagesverify_inconclusive, bills nothing and still ships the transcript. It convictsno_layeronly when the read is strong enough to prove the page held words. It callssuspect_layeronly with the same corruption evidence the defaults need. A live download is no longer cut at one minute: it runs until a minute passes with nothing arriving, so a throttled host's 60 MB scan completes where it used to time out. - 2026-10-09: a run stopped part-way through a document now lists the pages it never reached, marked
not_read, instead of dropping them; one stopped before it read anything comes back as anabortedrow. - 2026-10-08: pick where a page's text comes from, the layer or OCR every page; a cleanup pass for faded scans; a spending-limit stop lands as named rows with counts; an inconclusive check bills nothing; skipped and filtered entries now read
aborted, not a failure word. - 2026-10-07: the page check is a visible option again, billed per page checked.
- 2026-10-06: new inputs for download timeout, file-size cap, page layout on scans, word-level OCR lists and embedded images.
Limits
- 80 megabytes and sixty seconds a file, by default. A bigger or slower file gets a row saying why. The size and timeout inputs raise both.
- 300 pages a file, by default. A longer file is read up to the cap and marked truncated — raise the cap or give it a page range.
- Public links or your own files. A link behind a sign-in gets a row naming that; send the file itself instead.
- A refusing host is left alone. A 403 or a challenge page ends that host for the run; a 429 is waited out once. There's no proxy routing, on purpose.
- Copy protection stays closed without the file's owner password.
- The default check reads the layer, not the render. It measures each layer's statistics and whether its characters were painted where a reader can see them. Hidden text is flagged without a render, but a scan's transcript layer can't be told faithful from garbled and comes back
unverified. Verify pages checks a born-digital layer against the rendered page. It can't check a scan's own transcript either. - The page check won't pair with text-only mode. You get a row naming that refusal instead of a run half-keeping both.
- Option fields stay empty until you set their switch. Markdown, chunks, tables, phrase answers, images, word lists and the verify fields.
- OCR is OCR. A table needs the layer's layout, so an OCR-read page ships an empty tables list.
Troubleshooting
Fewer rows than entries? A repeated link or the same bytes is a duplicate row; an unchanged repeat is an already-returned row.
A failure row where a document should be? The miss code names the cause — a missing password, an over-size file, a refusing host — and the reason says what to fix.
A clean file flagged suspect? Slide decks and dense jargon carry the shapes the statistics check flags. Re-run it with the page check on.
Pages marked inconclusive under the page check? The OCR read was too weak to judge the layer: raise the resolution or fix the language and re-run.
FAQ
Can I use integrations? Yes. Schedule it on Apify and pick rows up through Make, Zapier or webhooks when a run finishes.
Can I use the Apify API? Yes. Call the run-sync endpoint with the JSON above.
Can I use it through an MCP server? Yes. The line under Use with AI agents adds the Actor to a compatible agent.
Is it legal to extract text from PDFs? The Actor reads the files you give it and stops there. The rights in the text are yours or your licensor's; the right to read each file, and what you do with the rows, is on you.
Can it read scanned PDFs? Yes. A page with no text layer is read by OCR inside the document price, and your row names which pages those were.
How much do 1,000 PDFs cost? $3.25 if every entry answers and fits in 50 pages, about $2.41 at the measured share.
How do I export the results? Rows land in your dataset as JSON, CSV or Excel, or over the API; images as PNGs in the run's store.
What are the limits? 100 entries a run, 300 pages a document, 80 MB and a minute of silence on the download. A host that keeps sending is given the time. Each is an input you can raise.
How accurate is the text?
A born-digital layer is the file's own text. A scanned page comes back at OCR quality in the language you picked. The page check settles a born-digital file you're unsure about. We can't check the text a scanner wrote into a file, so those pages come back unverified.
Where do I get support? Open an issue on this Actor's Issues tab. We answer within two days.
Feedback
A document that reads wrong or a field you need: open an issue on the Issues tab. If the Actor saved you time, a review on the Store helps other buyers find it.
See also
SEC EDGAR Filings Lookup Scraper · Broken Link Checker · Companies House Company Profile Scraper · Impressum Scraper
Not affiliated
PDF Text Extractor is an independent tool, not affiliated with, endorsed by or sponsored by any document host or standards body, including the example documents' hosts, the PDF format's steward and the Tesseract OCR project. It reads the files you point it at.