PDF Text Extractor: OCR, Markdown, Page Checks avatar

PDF Text Extractor: OCR, Markdown, Page Checks

Pricing

from $1.93 / 1,000 document returneds

Go to Apify Store
PDF Text Extractor: OCR, Markdown, Page Checks

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

Pradio Actors

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

2 days ago

Last modified

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

BuyerWhat you run it for
Search and AI pipeline buildersChecked text into indexes and embeddings
Compliance and legal teamsA check on every document before anyone reads it
Civic-data projectsScanned or broken files out of public PDF batches
Document-ingest operatorsWhich 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 unverified when 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 ExtractorMost-used alternative
Rows on the same document11
Price per 1,000 documents$3.25$5.00
Start charge per run$0.00005$0.005
A scanned page read by OCRinside the document price$0.015 a page
A verdict on every pageYesNo

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:

FieldWhat it holds
urlThe link this row answers; null on an inline file or repeat
file_nameThe link's last part, or the name you gave an inline file
download_urlThe 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
entryThe input line the row answers; on repeat rows, where url is null
statusagree or the dominant page flag, unverified when its pages could not be judged; the failure word on a miss row
recommended_actionOne plain sentence on what to do with the document; null on a miss
miss_codeThe cause code on a miss row, each one explained in Miss codes below; null on a document row
page_countPages the PDF declares
word_countWords in the delivered text
pdf_typetext_based, scanned, mixed or unreadable
pages_extractedPages that delivered text, by layer or OCR
titleThe PDF's metadata title; null when it declares none
createdThe PDF's creation date, ISO-8601; null when it carries none
modifiedThe PDF's last-modified date, ISO-8601; null when it carries none
file_size_bytesThe file's size in bytes
document_hashSHA-256 of the bytes read; null when no file was read
char_countCharacters in the delivered text, page joins included
textThe whole document text, a blank line between pages; null when Markdown alone is wanted
markdownThe document as basic Markdown; when the format asks
chunksPage-tagged text pieces for an index; under Chunk size
page_rangeThe range read, normalised; null on a whole document
must_contain_resultsEach required phrase: found, its pages, the first matching line
all_phrases_foundTrue when every required phrase was found
row_typeROW, ITEM_STATUS, NO_TEXT_FOUND or STOPPED_EARLY
reasonStatus rows: why the entry was not answered, the run was empty, or it stopped early
rowsFetchedStatus rows: rows fetched before de-duplication and the cap
rowsReturnedStatus rows: document rows in the dataset, 0 on an empty result
rowsRemainingStatus rows: fetched rows the run did not return
capped_entriesStatus rows: entries Maximum items held back, none read or billed
dropped_entriesStatus rows: entries the keep filter dropped, on the kept-out row
page_verdictsPer page: verdict — or null with unverified naming why the checks couldn't judge — which side delivered, the checks' scores, the text
per_page_methodPer page: embedded layer, OCR, or neither produced text
layer_ocr_agreementMean layer-versus-OCR agreement; under the page check
wrong_layer_pagesPages whose render showed text the layer lacks; under the page check
suspect_layer_pagesPages whose layer failed the statistics check
ocr_pagesPages whose text came from OCR
unreadable_pagesPages that gave no text from either side
verified_pagesPages the render check verified; empty without the page check
pages_truncatedTrue when the page cap cut the read short
low_confidence_pagesOCR pages under your confidence floor
ocr_capped_pagesPages still needing OCR after the per-document cap
verify_inconclusive_pagesChecked pages whose OCR read was too weak to judge the layer
tablesRebuilt tables with cells and a rebuilt-or-uncertain mark; when Extract tables is on
imagesEmbedded images saved as PNGs: page, link, size; when Extract embedded images is on
verdict_countsThe tally of page verdicts across the document
text_previewThe first 200 characters of the delivered text
metadataThe PDF's document-info dictionary; null when it carries none
processed_atWhen the row was produced, ISO-8601
processing_msMilliseconds this entry's own read took; null on an entry never reached
size_trimmed_fieldsWhat was shortened to fit one dataset row; null on almost every row
text_cleanupWhat the clean-up pass removed; when Clean up the text is on
extra_pagesExtracted 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 givenBilled rowsYou payIf every file answers
100about 74about $0.24$0.325
1,000about 742about $2.41$3.25
10,000about 7,424about $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?

  1. Press Try for free.
  2. Paste links into PDF links, or upload one PDF with the file picker.
  3. 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

InputTypeDefaultWhat it does
urlsarray3 example linksPDF links, one per line; Drive and Dropbox shares resolve
base64Pdfs*arraynoneFiles inline: a bare base64 string, or {name, data} plus a password
pdfFilefilenoneOne PDF picked in the Console, or its web URL over the API
inputDatasetId*stringnoneA saved dataset's id; anyone with it can read it
urlField*stringurlThe item column each link sits in
outputFormatselecttextWhole-document columns: text, markdown or both
keepRowsselectallWhich results come back: all, answered, flagged or missed
cleanTextbooleanoffDrop repeated headers and footers, page-number lines, split hyphens
extractTablesbooleanoffRebuild detected tables into rows of cells
extractImages*booleanoffSave embedded images as PNGs in the run's store
skipEmptyPages*booleanoffLeave proven-empty pages out of the page lists and counts
pageRangestringall pagesRead only these pages: 1-5,8, 10-, -3 for the last three
mustContain*arraynoneUp to 20 phrases every document must carry
pdfPasswordstringnoneOpens your protected PDFs; an entry's own password wins
ocrLanguageselectengThe language scans are read in; join codes with +
verifyPagesbooleanoffRender and OCR-check every page, $0.008 a page verified
pdfTextModeselectautotext never OCRs, ocr reads every page by OCR; text is refused under the page check
autoRotate*booleanonRetry a badly read scan turned 90, 180 and 270 degrees
dpi*integer144Render resolution for OCR, 72 to 400
preprocessModeselectnoneA cleanup the render gets before OCR: grayscale, contrast, binarise
minOcrConfidence*integernoneFlag OCR pages under this score, 0 to 100; the words stay, marked
segmentationModeselectautoThe page layout OCR assumes; orientation choices detect rotation
includeWords*booleanoffOCR pages list every word with confidence and position
chunkSize*integernoneSplit the text into chunks of about this size, 200 to 20,000
chunkOverlap*integer200Characters shared between chunks, at most half the size
maxItems*integer100The most rows a run returns, billed and unbilled both counting
maxPagesPerDocument*integer300The most pages one document contributes; more marks it truncated
ocrMaxPagesPerDocument*integernoneCap OCR pages a document; an over-cap page is marked, never skipped
maxConcurrency*integer5Documents 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*booleanoffOne document at a time, so the biggest files fit 1024 MB
requestTimeoutSecs*integer60Seconds one download may take, 5 to 600
maxDocumentSizeMb*integer80A file over this answers a too-large row, 1 to 250
onlyNew*booleanoffSkip 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_typeWhat it marks
ROWA document row
ITEM_STATUSAn entry the run could not answer; the miss code and reason say why
NO_TEXT_FOUNDA run with nothing to return
STOPPED_EARLYA 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 codeWhat it means
bad_urlThe line is blank, or it is not an http(s) link. Give a full link to the file
credentials_in_urlThe 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_requiredThe 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_pdfWhat came back is not a PDF. Check the link still points at the file
corrupt_pdfThe file starts like a PDF but cannot be read as one. Fetch it again, or take another copy
password_requiredThe 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_permittedThe file's own permissions forbid copying its text. Get the owner password, or a copy you are allowed to read
too_largeThe file is over maxDocumentSizeMb, 80 MB by default. Raise the cap, or send a shorter document
bad_page_rangeThe 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_base64An inline entry is not readable base64. Send { "name": "...", "data": "<base64>" }
bad_inputThe run refused the input itself, such as two options that cannot hold together. The reason names the option and what to change
read_errorThe PDF opened but could not be read. Fetch it again, or send another copy

The host or the network

Miss codeWhat it means
dns_failureThe host name does not resolve. Check the link's domain
connection_refusedThe host refused the connection. Check the port, or whether the service is up
host_unreachableThe host could not be reached at all. Check the network path
connection_resetThe host closed the connection mid-download. Try again later
timeoutNothing arrived for a minute. Raise requestTimeoutSecs, or try again
tls_errorThe host's HTTPS certificate was rejected. Fetch it over http, or fix the certificate
redirect_loopThe link redirects to itself. Give the final link
network_errorThe download failed for a reason we could not name. Try again
host_stoppedThis 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 codeWhat it means
duplicateThe same entry, or the same bytes under another name, is in this run twice. The first entry's row answers both
already_returnedUnder onlyNew, an earlier run already returned this file unchanged under the same options. It is not read or billed again
cap_reachedmaxItems held this entry back; capped_entries names every held entry. Raise the cap, or split the batch
kept_outYour 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
abortedThe 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_content without 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 answers agree; it carries a null verdict flagged unverified, and a document the checks cannot judge answers unverified rather than claiming the text is clean. Blank pages now agree with the empty page they render. suspect_layer needs 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 pages verify_inconclusive, bills nothing and still ships the transcript. It convicts no_layer only when the read is strong enough to prove the page held words. It calls suspect_layer only 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 an aborted row.
  • 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.