OCR a folder of scanned documents in one run
$0.006
a page: PDF, TIFF or photo
Folder in, text out
Set APIFY_TOKEN and put your files in scans.
Each scan with text becomes a Markdown file in text.
pip install apify-client
import mimetypes, os, re
from pathlib import Path
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
store = client.key_value_store(client.key_value_stores().get_or_create().id)
files = {}
try:
scans = [f for f in sorted(Path("scans").iterdir()) if f.is_file() and f.name[0] != "."]
for i, f in enumerate(scans):
key = f"{i}-" + re.sub(r"[^A-Za-z0-9._()'!-]", "-", f.name)
store.set_record(key, f.read_bytes(), content_type=mimetypes.guess_type(f.name)[0])
files[store.get_record_public_url(key)] = f.name
run = client.actor("brasspage/pdf-ocr-to-markdown").call(
run_input={"documentUrls": list(files)}
)
print("Run", run.status)
Path("text").mkdir(exist_ok=True)
for doc in client.dataset(run.default_dataset_id).iterate_items():
name = files.get(doc.get("url"))
if doc.get("markdown"):
Path("text", name + ".md").write_text(doc["markdown"], encoding="utf-8")
print(name, doc.get("pagesConverted"), "of", doc.get("pageCount"), doc.get("error") or doc.get("note") or "")
finally:
store.delete()
One real run: 3 files, 8 pages
I ran that exact script on Nov 7, 2026, on three public files. It
skipped the hidden .DS_Store that Finder leaves in
folders.
| File | Read from | Result |
|---|---|---|
| phototest.tif | OCR | 60 of 60 words |
| A made-up invoice scan, JPG | OCR | 33 of 33 numbers |
| The IRS W-9, 6 pages | Its text layer | The PDF's own text |
- Pages
- 8
- Seconds, start to last file
- 22
- Price of this run
- $0.04805
It printed 6 of 6, 1 of 1 and
1 of 1: pages read, of pages in the file. A lower first
number means a blank or failed page.
What happens to each page
- A page with real text is read as it is, so the characters are exact.
- A scanned page, photo, fax or image is rendered at 300 dpi, turned upright, deskewed and cleaned, then read by Tesseract 5.
- If Tesseract reads an English page badly, RapidOCR reads it too. Its text is used if it finds clearly more.
- A PDF that another tool already OCRed keeps that tool's hidden text, errors and all. If that text is tilted more than 0.8 degrees, the page is read again.
-
A file that fails, like a password-protected PDF, gets an
errorand no charge. The other files carry on.
Good at typed pages, weak at handwriting
| Pages | Result |
|---|---|
| Book scans, 60 pages, 5 handwritten | 96% of words |
| Turned pages, 306 test copies | 306 of 306 upright |
| Scanned invoices, 24 pages, 4 fonts | 810 of 810 numbers |
| Scanned tables with no grid lines | 74% of cells right |
| Phone photos of whole pages | 81% of words |
| Receipt photos | 67% of words |
| Handwritten notes | 52% of words |
Check these yourself: handwriting, phone photos, and pages with 2 or 3 text columns, where the first lines can mix the columns.
Big folders: four limits
-
Time: a run stops after 1 hour by default. The file
it is reading and all files after it give no text, and the script
prints the run's status. Import
timedeltafromdatetime, then addrun_timeout=timedelta(hours=3)tocall(). -
Pages: the Apify free plan stops a run after 20
pages, and allows 50 pages a month. Paid plans stop at 1,000 pages a
run, unless you add
maxPagesPerRuntorun_input. - Files: PDF, PNG, JPEG, WEBP and TIFF, up to 100 MB, 14 MB a page and 5,000 pages each. Export Word, Excel and PowerPoint files to PDF first.
-
Spend: add
max_total_charge_usdtocall()to cap what a run costs.
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Do my scans go to an LLM?
No. Open source OCR runs inside the Actor: Tesseract, plus RapidOCR for pages that Tesseract reads badly. The script deletes its upload store at the end, and the text stays in your own Apify storage.
Can I get plain text instead of Markdown?
Yes. Add "outputFormat": "text" to
run_input, and change both
"markdown" keys in the loop to "text".
Which languages does it read?
English by default. For Spanish, add
"language": "spa" to run_input, or fra,
deu, por, rus, ara, hin, chi_sim or jpn for French, German,
Portuguese, Russian, Arabic, Hindi, Chinese (simplified) or
Japanese. One run reads one language, and we have no accuracy
number for other languages.
Do I need Python?
No. In the Apify Console, paste file links into
documentUrls, or upload one file. From other code,
see the PDF OCR API.
Where do these numbers come from?
-
The run:
IV2m9gomOTlhSBSewon build 0.1.44, the live build, at the default 1 GB. Apify timed the run itself at 13.3 seconds. - The price uses the Store prices: one start at $0.00005 and 8 pages at $0.006. As the Actor's owner, I was not charged.
-
The 60 words match
phototest.txt, the truth file next to the scan. The invoice output matches the README's own run. - The README rows: the invoices and tables are made-up test files. The photo, receipt and note rows are real photos from GNHK, CORD and SmartDoc 2015.