# Scanned PDF OCR to Page Text (`automation-lab/scanned-pdf-ocr-text`) Actor

Recognize English text from public scanned PDFs and export page-numbered OCR text, language, status and document provenance for indexing.

- **URL**: https://apify.com/automation-lab/scanned-pdf-ocr-text.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.28 / 1,000 ocr page with texts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Scanned PDF OCR to Page Text

Turn image-only PDF pages into searchable English text. This scanned PDF OCR Actor downloads anonymous public PDF URLs, renders pages with PDFium and recognizes text locally with Tesseract. It returns one row per processed page, with page number, source URL, document hash, language model and status.

Use it for recurring document indexing, searchable archives and RAG ingestion. No external OCR account or AI subscription is needed. It does not create searchable PDF files or edit the original documents.

### Who is it for?

- Search engineers indexing scanned manuals and reports.
- Archive operators importing recurring public document collections.
- Automation builders who need page-level provenance alongside recognized text.

### Why use it?

Text-layer extractors cannot read a scan that contains only page images. This Actor actually renders and recognizes those pages. Document hashes let your downstream pipeline identify changed source bytes. Explicit blank and failure rows distinguish an empty page from a download failure.

The output is plain OCR text, not a promise of perfect layout, transcription accuracy, proofreading or table reconstruction.

### Getting started

1. Supply a direct public PDF URL under **Public PDF URLs**.
2. Choose a global output-record cap and a page cap per PDF.
3. Start the Actor and wait for completion.
4. Open **OCR pages** in the output dataset.
5. Export JSON, CSV or Excel using Apify dataset exports.

Example input (a public scanned product brochure used by the OCRmyPDF test suite):

```json
{
  "startUrls": [{"url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf"}],
  "maxItems": 20,
  "maxPagesPerDocument": 10
}
```

The example PDF has one page; output volume follows the actual source, not the requested cap.

### Inputs and limits

| Input | Default | Meaning |
|---|---|---|
| startUrls | Required | 1–20 unique anonymous HTTP(S) PDF URLs |
| maxItems | 20 | Global dataset-row cap, 1–200; includes blank/error rows |
| maxPagesPerDocument | 10 | First 1–50 pages of each PDF |

Documents and pages are processed sequentially in input order. Both limits apply together. Later pages or documents are intentionally skipped when either cap is reached. There is no zero/unlimited setting. Runtime and spending limits can stop collection earlier; inspect the completion counters in the run log.

Each URL entry accepts only `url` and optional `method: "GET"`. Other methods, headers, payload and userData are rejected rather than silently ignored.

Each PDF is limited to 20 MB. Responses must have PDF or binary content type and start with the PDF signature. At most five redirects are followed, with public-destination checks at every hop. Transient transfers receive at most two retries; permanent access errors are not retried. Each transfer has a 30-second deadline. Page rendering and OCR have a 90-second parent deadline, including a 60-second OCR deadline.

Pages are rasterized at up to 200 DPI, with the longest side capped at 2500 pixels. PDF page dimensions above 14,400 points are rejected. These bounds control resource use but may lose small-print detail.

### Extracted data

| Field | Meaning |
|---|---|
| sourceUrl | Original supplied PDF URL |
| documentIndex | One-based input position |
| documentSha256 | Hash of downloaded PDF bytes; null before download succeeds |
| pageNumber | One-based PDF page; null for document errors |
| pageCount | Full source page count, including pages skipped by limits |
| text | Recognized page text; null on blank or failed pages |
| language | Always `eng`, the OCR model; not detected language |
| status | `succeeded`, `no_text` or `failed` |
| error | Bounded explanation on failure, otherwise null |
| processedAt | UTC processing timestamp |

The default dataset contains all output. A document download/parse error produces one diagnostic row with no page number. A blank page produces `no_text`. Mixed batches retain successful results; a run with failures and no useful page fails instead of pretending extraction succeeded. An entirely blank, valid document can complete with only `no_text` rows.

### Output example

The example PDF begins with this recognized text (abbreviated here; the dataset retains the full OCR text):

```json
{
  "sourceUrl": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf",
  "documentIndex": 1,
  "pageNumber": 1,
  "pageCount": 1,
  "language": "eng",
  "status": "succeeded",
  "text": "The LinnSequencer\n32 Track MIDI Sequence Recorder",
  "error": null
}
```

Production rows also include the document hash and processing timestamp described above.

### How much does it cost to recognize scanned PDF pages?

Pay-per-event pricing includes one **start** event per run and one **item** event per page that produces nonempty OCR text. Blank and failed pages have no item charge; the start fee still applies. Spending caps are checked before work and before accepting each paid page. A capped run may return only partial results.

The start fee is $0.001 per run. Per nonempty OCR page: FREE $0.00437; BRONZE $0.0038; SILVER $0.002964; GOLD, PLATINUM and DIAMOND $0.00228. Spend tiers follow qualifying aggregate monthly Apify Store spend, not the number of pages in this Actor.

BRONZE estimated run totals (start fee included): $0.0048 for 1 useful page; $0.039 for 10; $0.381 for 100. Blank/error diagnostic rows add no per-page fee. These examples assume completed useful output and no spending-cap interruption.

### Integrations

- **RAG ingestion:** store `documentSha256`, `pageNumber` and `sourceUrl` beside chunks in your vector database.
- **Search indexes:** index only rows whose status is `succeeded`; route failure rows to an operational queue.
- **n8n and Make:** schedule the same public URLs, fetch the default dataset after completion, then deduplicate hashes downstream.
- **Spreadsheets:** export page text and statuses to CSV or Excel for review.

Scheduling is supplied by Apify or your orchestration system. This Actor does not discover new PDFs, compare past runs, monitor changes or send alerts itself.

### API usage

Start a run once, retain the returned run and dataset IDs, and retrieve results after successful completion. Do not restart a run just to check its status.

```bash
curl -X POST 'https://api.apify.com/v2/acts/automation-lab~scanned-pdf-ocr-text/runs' \
  -H "Authorization: Bearer $APIFY_TOKEN" -H 'Content-Type: application/json' \
  -d '{"startUrls":[{"url":"https://example.org/scan.pdf"}],"maxItems":20}'
```

JavaScript with `apify-client`:

```js
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/scanned-pdf-ocr-text').call({
  startUrls: [{ url: 'https://example.org/scan.pdf' }], maxItems: 20
});
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 20 });
```

Python with `apify-client`:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/scanned-pdf-ocr-text').call(
    run_input={'startUrls': [{'url': 'https://example.org/scan.pdf'}], 'maxItems': 20})
items = client.dataset(run['defaultDatasetId']).list_items(limit=20).items
```

Replace the illustrative URL with a real public PDF you are permitted to process. Import `ApifyClient` from `apify-client` in your JavaScript application.

### MCP use

Use the stable Actor-specific hosted endpoint:

```bash
claude mcp add --transport http apify "https://mcp.apify.com?tools=automation-lab/scanned-pdf-ocr-text"
```

Claude Desktop and Cursor: use this configuration in an HTTP-compatible MCP client. VS Code: add the same Actor URL under its MCP HTTP server configuration.

Equivalent configuration for compatible HTTP clients:

```json
{"mcpServers":{"apify":{"url":"https://mcp.apify.com?tools=automation-lab/scanned-pdf-ocr-text"}}}
```

Authenticate using your client's supported Apify authorization flow. Discover actual tool names and argument schemas with `tools/list`; selecting an Actor also exposes run and storage tools, not exactly one tool.

Example prompts: “Recognize the first page of this public scan” or “Export page text with source provenance.”

Example request: “Read the first two scanned pages from this public PDF and return the text with page provenance.” Only execute after authorizing the invocation. Start once and keep its run ID. If nonterminal, check that same run with bounded backoff (2, 4, 8 seconds, capped at 10) and a 120-second consumer deadline. On deadline report pending, not success; never restart just to poll.

After success, retrieve dataset pages with explicit offset, limit and required fields. An example consumer budget is 20 rows per page, at most 100 source rows and 64 KiB serialized UTF-8 content admitted to model context. Enforce bytes host-side and disclose omitted/oversized content. Without interception do not claim a hard byte guarantee. Report continuation offset and truncation; keep full exports outside model context. Run/storage IDs are metadata, not OCR results. No universal token-saving claim is made.

### Accuracy and limitations

English machine-printed text is supported. Handwriting, poor contrast, rotation, damaged scans, dense tables, columns, small print and unusual fonts may reduce accuracy or change reading order. No confidence score, bounding boxes, translation, detected language, tables, Markdown, DOCX or searchable PDF artifact is produced.

Digital PDFs are rasterized and OCRed too, rather than using their text layer. For precise existing text-layer extraction use our related PDF Text Extractor. Password-protected/encrypted files and private destinations are unsupported. No proxy, login, cookies, authenticated URLs or upload mode is provided.

### Legality and responsible use

Only process documents you are authorized to download and analyze. Public availability does not grant permission to republish copyrighted text or personal information. You control dataset access and retention in Apify.

PDF bytes and page images are temporary and removed after each document. OCR runs locally inside the Actor; no external recognition API receives page content. Failed operations send sanitized diagnostic input, exceptions and actor/build/run IDs to our private GlitchTip service for repair; secret fields and URL queries are removed and reports are retained for 30 days. Source URL inputs are additionally redacted from telemetry. Your dataset still contains supplied source URLs and extracted text.

### FAQ and troubleshooting

**Why did it return no text?** A valid blank page returns `no_text`; a low-quality or non-English scan may also have no recognizable English text. Inspect the original scan before interpreting empty output as missing content.

**Why did downloading fail?** Use a direct anonymous PDF URL, not a sign-in page or viewer. Check the 20 MB limit, MIME type, encryption and public-address requirements. Protected sites are not bypassed automatically.

**Why are pages missing?** Check both input caps, spending cap and runtime. Compare the returned `pageCount` with processed `pageNumber` values. Partial output is not a claim of full-document completion.

**Can I edit or proofread a PDF?** No. The Actor produces machine-recognized plain text and provenance, not a human-guaranteed transcription or redesigned document.

### Related Actors

- [PDF Text Extractor](https://apify.com/automation-lab/pdf-text-extractor): extract a PDF's existing text layer instead of recognizing its page images.
- [Batch Image Text OCR](https://apify.com/automation-lab/batch-image-text-ocr): use image inputs and richer block output rather than native PDF page ingestion.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/scanned-pdf-ocr-text/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

1–20 unique public HTTP(S) PDF URLs, processed in input order. Public IPs only, including redirects. PDF or binary MIME required, maximum 20 MB/file. Anonymous access only; authentication is unsupported. Each entry accepts only url and optional method GET; other methods, headers, payload and userData are rejected.

## `maxItems` (type: `integer`):

Global run cap on dataset rows, including blank pages and error rows. Stops in source/page order; does not imply full PDF coverage. Zero/unlimited is unsupported.

## `maxPagesPerDocument` (type: `integer`):

Reads pages 1 through this inclusive cap for each PDF, also subject to global maxItems, runtime and spending caps. Later pages are intentionally skipped.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf"
    }
  ],
  "maxItems": 20,
  "maxPagesPerDocument": 10
}
```

# Actor output Schema

## `dataset` (type: `string`):

Default dataset containing page and document-error rows.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/scanned-pdf-ocr-text").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "startUrls": [{ "url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/scanned-pdf-ocr-text").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/linn.pdf"
    }
  ]
}' |
apify call automation-lab/scanned-pdf-ocr-text --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/scanned-pdf-ocr-text"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/qI5JM2fqbjah5UT78/builds/5J8Z1faJDjOR9QAXk/openapi.json
