How to OCR Scanned PDFs to Text with an API

Uses PDF OCR — Scanned PDF to Searchable Text (29 Languages)

Short answer: Send scanned PDF links to the PDF OCR actor, get text back one row per page with a confidence score, and use that score to flag pages for review. OCR costs $3 per 1,000 pages; pages that already have text cost $0.30 per 1,000.

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

  1. 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.

  2. Choose languages and PDF mode

    Pick only the languages your documents use, up to 4, using Tesseract codes such as eng, deu, fra or chi_sim. Fewer languages is faster and more accurate. Keep pdfMode on auto so pages that already contain text are read directly at the cheaper price; switch to ocr-all only 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
    }
  3. Tune for your document type

    Leave dpi at 300 for normal scans; 200 is faster for large print. pageSegmentation defaults to auto; use single-column for uniform book-like text and sparse for forms with scattered fields. Setting minWordConfidence to around 40-60 removes most noise from stains and stamps. Raise maxPagesPerPdf above the default 200 if your documents are longer, or they will be marked truncated.

  4. Flag low-confidence pages for review

    With outputMode set to pages, every row is one page with page, confidence, wordCount and text. 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}")
  5. 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 outputMode to document to 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)
  6. 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 urls list, and defaults to one row per file. Pricing is identical.

  7. 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.

urlfileTypepagesOcrconfidencewordCounttextpagesTextLayer
https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdfpdf195.5728The LinnSequencer\n32 Track MIDI Sequence Recorder\n\nThe LinnSequencer is a state-of-the-art composition and performance tool…
https://arxiv.org/pdf/1706.03762pdf06457Provided 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 forWhenPricePer 1,000Worked example
Page ocrCharged per image or scanned PDF page read by OCR$0.003$3A 10-page scanned PDF = $0.03; 1,000 receipts = $3.00
Page textCharged per PDF page that already has a text layer (read directly, no OCR)$0.0003$0.30A 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 page

Tips and pitfalls

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 ocr

OCR — 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 ocr

Related 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…

All guides