A scanned PDF has no text to copy or search, only page images. Getting text out of it means rendering each page, running OCR in the right language and stitching the result back together, and doing that reliably for thousands of files is where most of the work goes.
Running Tesseract yourself is free but needs a renderer, language packs, a queue and servers. Desktop tools work one file at a time. The PDF OCR actor wraps Tesseract 5 behind an API, reads pages that already have a text layer without OCR, and reports a confidence score for every OCR'd page.
This guide sets up a page-level OCR run, checks the quality of the result in Python, rebuilds full documents from page rows, and shows when to use the mixed-format OCR to Text actor instead.
Step by step
Gather the PDF links and any password
Each PDF must be reachable by URL. Files up to 100 MB and up to 2,000 pages are supported. If some PDFs are password-protected and you are entitled to open them, put the password in
password; it applies to every PDF in that run, so group files that share a password.Choose languages and PDF mode
Pick only the languages your documents use, up to 4, using Tesseract codes such as
eng,deu,fraorchi_sim. Fewer languages is faster and more accurate. KeeppdfModeonautoso pages that already contain text are read directly at the cheaper price; switch toocr-allonly for PDFs whose hidden text layer is known to be garbage.{ "urls": [ "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf" ], "languages": ["eng", "deu"], "pdfMode": "auto", "outputMode": "pages", "dpi": 300, "maxPagesPerPdf": 500, "minWordConfidence": 40 }Tune for your document type
Leave
dpiat 300 for normal scans; 200 is faster for large print.pageSegmentationdefaults toauto; usesingle-columnfor uniform book-like text andsparsefor forms with scattered fields. SettingminWordConfidenceto around 40-60 removes most noise from stains and stamps. RaisemaxPagesPerPdfabove the default 200 if your documents are longer, or they will be marked truncated.Flag low-confidence pages for review
With
outputModeset topages, every row is one page withpage,confidence,wordCountandtext. Clean printed scans usually score above 95; handwriting, stamps and low-resolution scans score lower. Sort by confidence and send the weakest pages to a human.import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) run = client.actor("actor-forge/pdf-ocr").call(run_input={ "urls": ["https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf"], "languages": ["eng"], "outputMode": "pages", }) rows = list(client.dataset(run["defaultDatasetId"]).iterate_items()) THRESHOLD = 80 for r in rows: if r.get("error"): print("ERROR", r.get("url"), r["error"]) continue conf = r.get("confidence") # empty for text-layer pages if conf is not None and conf < THRESHOLD: print(f"REVIEW {r['url']} page {r.get('page')}: confidence {conf}")Rebuild one text file per PDF
Page rows are ideal for page-level search, but you often also want one text file per document. Group rows by URL, sort by page number and join them. Alternatively set
outputModetodocumentto get one row per PDF directly.from collections import defaultdict from pathlib import Path import hashlib pages = defaultdict(list) for r in rows: if r.get("text"): pages[r["url"]].append((r.get("page") or 0, r["text"])) out = Path("ocr_text") out.mkdir(exist_ok=True) for url, items in pages.items(): name = hashlib.sha1(url.encode()).hexdigest()[:12] + ".txt" text = "\n\n\f\n\n".join(t for _, t in sorted(items)) # form feed between pages (out / name).write_text(text, encoding="utf-8") print(name, "<-", url)Use OCR to Text for mixed PDFs and images
If the same pipeline also receives photos, TIFF faxes or screenshots, send everything to OCR to Text instead. It takes the same inputs, accepts PDF and image formats in one
urlslist, and defaults to one row per file. Pricing is identical.Cap the cost of big batches
Set a maximum total charge on the run. Processing stops cleanly at the cap and no page is charged twice, so an unexpectedly long archive cannot overrun your budget.
maxFiles(up to 10,000) gives a second limit on the number of files per run.
Sample input
{
"urls": [
"https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf",
"https://arxiv.org/pdf/1706.03762"
],
"languages": [
"eng"
],
"pdfMode": "auto",
"outputMode": "pages",
"maxPagesPerPdf": 200
}Sample output
The sample shows a scanned PDF that needed OCR and a digital PDF read entirely from its text layer, with the page counts for each method.
| url | fileType | pagesOcr | confidence | wordCount | text | pagesTextLayer |
|---|---|---|---|---|---|---|
| https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf | 1 | 95.5 | 728 | The LinnSequencer\n32 Track MIDI Sequence Recorder\n\nThe LinnSequencer is a state-of-the-art composition and performance tool… | ||
| https://arxiv.org/pdf/1706.03762 | 0 | 6457 | Provided proper attribution is provided, Google hereby grants permission to reproduce the tables and figures… | 15 |
Run it from code
cURL
curl -X POST "https://api.apify.com/v2/acts/actor-forge~pdf-ocr/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf", "https://arxiv.org/pdf/1706.03762"], "languages": ["eng"], "pdfMode": "auto", "outputMode": "pages", "maxPagesPerPdf": 200}'JavaScript (Node.js)
// Node.js 18+ (built-in fetch). Set APIFY_TOKEN in your environment.
const input = {
"urls": [
"https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf",
"https://arxiv.org/pdf/1706.03762"
],
"languages": [
"eng"
],
"pdfMode": "auto",
"outputMode": "pages",
"maxPagesPerPdf": 200
};
const res = await fetch(
"https://api.apify.com/v2/acts/actor-forge~pdf-ocr/run-sync-get-dataset-items",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
const items = await res.json();
console.log(items.length, items[0]);Python
# pip install apify-client (set APIFY_TOKEN in your environment)
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"urls": [
"https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf",
"https://arxiv.org/pdf/1706.03762"
],
"languages": [
"eng"
],
"pdfMode": "auto",
"outputMode": "pages",
"maxPagesPerPdf": 200
}
run = client.actor("actor-forge/pdf-ocr").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)The run-sync-get-dataset-items endpoint waits for the run (up to 5 minutes) and returns the dataset rows directly. For longer runs, start the run asynchronously and read the dataset when it finishes, as the Python example does. Replace APIFY_TOKEN with your own token from the Apify console.
What it costs
An archive of 400 scanned PDFs averaging 12 pages is 4,800 OCR pages, so 4,800 x $0.003 = $14.40. A 200-page PDF where only 20 pages are scanned costs 20 x $0.003 + 180 x $0.0003 = $0.06 + $0.054 = $0.114.
| Charged for | When | Price | Per 1,000 | Worked example |
|---|---|---|---|---|
| Page ocr | Charged per image or scanned PDF page read by OCR | $0.003 | $3 | A 10-page scanned PDF = $0.03; 1,000 receipts = $3.00 |
| Page text | Charged per PDF page that already has a text layer (read directly, no OCR) | $0.0003 | $0.30 | A 100-page digital PDF = $0.03 |
| Actor Start (platform start fee) | Charged when the Actor starts running. Number of events charged depends on Actor memory (one event per GB, minimum one event). | $0.00005 | — | One run at 4 GB = $0.0002 |
Ready to try it? PDF OCR — Scanned PDF to Searchable Text (29 Languages) runs on Apify, pay per result.
Run on Apify → Read the full actor pageTips and pitfalls
- Rotate pages that are sideways or upside down before submitting; 90 and 180 degree rotations are not corrected automatically.
- Tables come out as lines of text, not structured tables. Plan a separate extraction step if you need table cells.
- Modern Greek is not available, and handwriting comes out poorly. Use the confidence score to find pages that need a human.
- Links that return a web page instead of a file are reported and not charged; check for login walls on document links.
FAQ
Does the API return a searchable PDF?
No. It returns the recognised text (and optionally word boxes) as dataset rows. Store the text in your search index next to the original PDF, or use the word boxes if you want to build an overlay.
Is the OCR confidence per page or per word?
Each OCR'd page has a confidence score. With includeWords on, every word also gets its own confidence and pixel box. Pages read from an existing text layer have no confidence because no OCR was needed.
Can I OCR PDFs in Chinese, Japanese or Arabic?
Yes. The 29 supported languages include simplified and traditional Chinese, Japanese, Korean, Arabic, Hebrew, Hindi and Thai. Select up to 4 per run in languages.
Should I use PDF OCR or OCR to Text?
Same engine and same prices. PDF OCR defaults to one row per page, which suits scanned PDF archives. OCR to Text defaults to one row per file and is the better fit when PDFs and images arrive mixed.
Actors used in this guide
PDF OCR — Scanned PDF to Searchable Text (29 Languages)
OCR scanned PDFs to text page by page with Tesseract 5 in 29 languages. Pages that already have a text layer are read directly at a tenth of the…
$3 per 1,000 · page ocrOCR — Scanned PDF & Image to Text (Tesseract, 29 Languages)
Extract text from scanned PDFs and images (PNG, JPEG, TIFF, WebP, GIF, BMP) with Tesseract 5 OCR in 29 languages. PDF pages that already have text…
$3 per 1,000 · page ocrRelated guides
How to Convert PDF to Markdown for RAG Pipelines
Send your PDF links to the Any URL to Markdown actor, read the markdown field of each result, then split it by heading into token-sized chunks…
How to Extract Text from Images and Screenshots in Bulk
Send image URLs to the Image to Text OCR actor, use the sparse segmentation mode for screenshots, and read the text and confidence per image. It…