# PDF Text Extractor + OCR for Scanned PDFs (`sumitr_mardy/pdf-text-extractor`) Actor

Extract text from PDFs by URL, with automatic OCR for scanned pages. One flat row per URL, never null, only successful PDFs billed. Fast, low-memory engine; Google Drive, Dropbox and arXiv links accepted. Built for pipelines and AI agents.

- **URL**: https://apify.com/sumitr_mardy/pdf-text-extractor.md
- **Developed by:** [Sumitr Mardy](https://apify.com/sumitr_mardy) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 pdf extracteds

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

## PDF Text Extractor + OCR for Scanned PDFs

Give it PDF links, get back the text. Pages that are scans or photos are read with OCR automatically; pages that already have text are never OCRed. One flat row per URL, every field always present, never `null` — built to be called from pipelines, n8n/Make/Zapier flows and AI agents.

**$2 per 1,000 PDFs. $5 per 1,000 OCR pages. Failed downloads, broken files and PDFs with no text are free.**

### Why this one

- **It does not fall over on scans.** OCR runs one page at a time with bounded memory. On a 41-file batch with 120 scanned pages, peak memory was 541 MB at the default 2 GB.
- **One row for every URL you send.** A 404, a password-protected file and a corrupt PDF each still come back as a row with `status: "failed"` and a machine-readable `subStatus`. You never have to diff your input against the output to find what went missing.
- **You pay for text, not for attempts.** A PDF is billed only when text came out of it. An OCR page is billed only when OCR recovered more text than the page already had.
- **Fast.** The text layer is read with PDFium, the PDF engine inside Chrome: 1–8 ms per page. An 838-page book took 18 seconds end to end, including the 52 MB download.
- **Share links just work.** Google Drive, Dropbox, OneDrive and arXiv `/abs/` links are turned into direct downloads. If a host blocks the request (401/403/407/429), the download is retried once through Apify Proxy.

### Input

| Field | Default | What it does |
|---|---|---|
| `urls` | — | PDF links, one per line. Required. |
| `ocrMode` | `auto` | `auto` OCRs only pages with no text layer that contain an image. `off` never OCRs. |
| `ocrLanguage` | `eng` | Tesseract language code; join several with `+` (`eng+fra`). 30 languages are built in, including `vie`, `chi_sim`, `jpn`, `kor`, `ara`, `hin`. |
| `maxOcrPagesPerPdf` | `50` | Cost cap for huge scanned books. Pages over the cap come back empty and are listed in `notes`. |
| `pageRange` | all | `1-5, 8, 12-` style. A range that selects no page fails that row, uncharged. |
| `includePages` | `false` | Adds `pages[]` with per-page text and an `ocr` flag, for page-level citations. |
| `chunkSize` / `chunkOverlap` | `0` / `200` | RAG chunks of about `chunkSize` characters, each with `pageStart` and `pageEnd`. `0` turns chunking off. |
| `password` | — | Used for every PDF in the run that needs one. |
| `maxFileSizeMb` | `100` | Larger files fail as `too_large`, uncharged. |

```json
{
  "urls": ["https://arxiv.org/abs/1706.03762", "https://www.irs.gov/pub/irs-pdf/f1040.pdf"],
  "ocrLanguage": "eng",
  "chunkSize": 1500
}
```

### Output

One row per input URL, in completion order. Use `inputIndex` to line rows up with your input.

```json
{
  "inputIndex": 0,
  "url": "https://arxiv.org/abs/1706.03762",
  "resolvedUrl": "https://arxiv.org/pdf/1706.03762.pdf",
  "fileName": "1706.03762.pdf",
  "status": "ok",
  "subStatus": "text_layer",
  "error": "",
  "pageCount": 15, "pagesExtracted": 15, "ocrPages": 0, "scannedPages": 0,
  "charCount": 39598, "wordCount": 6099,
  "text": "Attention Is All You Need\nAshish Vaswani∗\nGoogle Brain\n…",
  "textTruncated": false, "fullTextUrl": "",
  "pages": [], "chunks": [],
  "title": "", "author": "", "subject": "", "keywords": "", "creator": "LaTeX with hyperref", "producer": "pdfTeX-1.40.25",
  "creationDate": "2024-04-10T21:11:43Z", "modDate": "2024-04-10T21:11:43Z",
  "pdfVersion": 15, "isEncrypted": false, "fileSizeBytes": 2215244,
  "notes": [], "processingMs": 218
}
```

Strings are `""` when absent, numbers `0`, arrays `[]`. No field is ever `null` or missing.

#### Status

| `status` | `subStatus` | Charged |
|---|---|---|
| `ok` | `text_layer` — all text came from the PDF itself | PDF |
| `ok` | `ocr` — some or all pages were read by OCR | PDF + each OCR page |
| `ok` | `ocr_incomplete` — some scanned pages were not read (page cap, cost limit, OCR off, OCR failure); see `notes` | PDF + each OCR page |
| `no_text` | `no_text_layer`, `scanned_ocr_off`, `ocr_found_nothing` | free |
| `failed` | `http_404` (any `http_<code>`), `not_a_pdf`, `too_large`, `download_timeout`, `download_error`, `password_required`, `wrong_password`, `unsupported_encryption`, `corrupt_pdf`, `empty_document`, `bad_page_range`, `page_range_out_of_bounds`, `invalid_url`, `cost_limit_reached`, `extraction_error` | free |

Very large documents: if a row would exceed Apify's dataset item limit, the full text is saved to the run's key-value store and linked from `fullTextUrl`. `text` keeps the first 1,000,000 characters and `textTruncated` is `true`.

### Pricing

| Event | Price | When |
|---|---|---|
| `pdf-extracted` | $0.002 | A PDF returned text (`status: "ok"`) |
| `ocr-page` | $0.005 | A scanned page whose text was recovered by OCR |

The only other charge is Apify's standard Actor start fee, $0.00005 per GB of memory ($0.0001 per run at the default 2 GB). Prices drop on higher Apify plans: down to $1.50 per 1,000 PDFs and $3.50 per 1,000 OCR pages on Gold. A run stops starting new PDFs when your *maximum cost per run* is reached, and marks the rest `cost_limit_reached`, uncharged.

### What it does not do (yet)

Honest limits, so you can decide before you run it:

- **Plain text, not Markdown.** Line breaks are kept and pages are joined with a blank line, but headings and lists are not marked up.
- **Tables come out as text,** one table row per line. Cells are not split into columns.
- **Text order follows the PDF's own content order.** For most papers, reports and forms that is reading order. Some designed layouts (magazines, brochures) can interleave columns.
- **OCR is Tesseract with fast models.** It is good on clean scans and weaker on handwriting, very low-resolution photos and heavily stylised fonts.
- **Text-based PDFs only need small memory. Scans need CPU.** The default 2 GB OCRs about one page every 4–5 seconds. For big scanned batches, 8 GB runs two OCR pages at a time.

### Use it from AI agents (MCP)

This Actor is a tool on the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, Cursor, VS Code or any MCP client can call it to read a PDF. Load only this tool: a short tool list keeps the agent from picking the wrong one.

**Recommended: hosted server, OAuth sign-in (nothing secret on disk).** Add to your MCP client config (`claude_desktop_config.json`, `.cursor/mcp.json`, …):

```json
{
  "mcpServers": {
    "pdf-text-extractor": {
      "url": "https://mcp.apify.com?tools=sumitr_mardy/pdf-text-extractor"
    }
  }
}
```

**With an API token instead** (clients without OAuth, or unattended setups):

```json
{
  "mcpServers": {
    "pdf-text-extractor": {
      "url": "https://mcp.apify.com?tools=sumitr_mardy/pdf-text-extractor",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

**Local server (stdio):**

```json
{
  "mcpServers": {
    "pdf-text-extractor": {
      "command": "npx",
      "args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "sumitr_mardy/pdf-text-extractor"],
      "env": { "APIFY_TOKEN": "YOUR_APIFY_TOKEN" }
    }
  }
}
```

The hosted server also gives the agent this Actor's output schema, so it knows every field before the first call. `apify mcp install <client>` writes the config for you if you use the Apify CLI. Get a token at [Apify Console → API & Integrations](https://console.apify.com/settings/integrations), and never commit it.

**Tips for agents:**

- *"Summarise this PDF"*: pass the link in `urls`. Read `text`, and check `status` first: `no_text` and `failed` rows carry the reason in `subStatus` and `error`.
- *"Find where the report says X"*: set `includePages: true` and quote `pageNumber` back to the user, or set `chunkSize: 1500` and search the `chunks`.
- Long scanned documents: set `pageRange` or a lower `maxOcrPagesPerPdf`. OCR takes about 4–5 seconds a page at the default memory.
- The MCP `call-actor` tool waits up to 45 seconds. A longer run keeps going: fetch the results with `get-dataset-items` and the run's `datasetId`.
- Every row has the same keys and never a `null`, so an agent can read `text`, `pageCount` or `title` without a guard. An empty `title` means the PDF has no title in its metadata, not that the read failed.

### Use it from code

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("sumitr_mardy/pdf-text-extractor").call(run_input={
    "urls": ["https://www.irs.gov/pub/irs-pdf/f1040.pdf"],
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["status"], row["subStatus"], row["charCount"])
```

```bash
curl -X POST "https://api.apify.com/v2/acts/sumitr_mardy~pdf-text-extractor/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://arxiv.org/abs/1706.03762"]}'
```

# Actor input Schema

## `urls` (type: `array`):

Links to PDF files, one per line. Direct links, Google Drive, Dropbox and OneDrive share links, and arXiv /abs/ pages are all accepted. Every URL gets exactly one output row, including the ones that fail.

## `ocrMode` (type: `string`):

Auto runs OCR only on pages with no text layer that contain an image (scans, photos of documents). Pages that already have text are never OCRed. Only pages where OCR actually recovers text are charged.

## `ocrLanguage` (type: `string`):

Tesseract language code; join several with + (for example eng+fra). Available: eng, fra, deu, spa, por, ita, nld, pol, ces, ron, hun, swe, dan, nor, fin, tur, rus, ukr, ell, vie, ind, msa, tha, chi_sim, chi_tra, jpn, kor, ara, heb, hin.

## `maxOcrPagesPerPdf` (type: `integer`):

Upper bound on pages OCRed in one PDF, to keep the cost of a huge scanned book predictable. Scanned pages beyond it are returned without text and reported in notes.

## `pageRange` (type: `string`):

Extract only these pages, for example 1-5, 8, 12- (12- means page 12 to the end). Empty extracts every page.

## `includePages` (type: `boolean`):

Add a pages array with each page number, its text, and whether the text came from OCR. Useful for page-level citations.

## `chunkSize` (type: `integer`):

Split the text into chunks of about this many characters, each carrying the pages it spans. 0 turns chunking off.

## `chunkOverlap` (type: `integer`):

Characters of the previous chunk repeated at the start of the next one, so a sentence cut at a boundary still appears whole once.

## `password` (type: `string`):

Password for protected PDFs. Applied to every PDF in the run that needs one.

## `maxFileSizeMb` (type: `integer`):

Larger files are skipped and reported as failed (too_large), not charged.

## `downloadTimeoutSecs` (type: `integer`):

Time allowed per download. Raise it for very large files on slow hosts.

## `maxConcurrency` (type: `integer`):

How many PDFs are downloaded and processed at once.

## `proxyConfiguration` (type: `object`):

Downloads go direct by default. If a host answers 401/403/407/429, the download is retried once through Apify Proxy automatically. Turn this on to always download through a proxy.

## Actor input object example

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
    "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf"
  ],
  "ocrMode": "auto",
  "ocrLanguage": "eng",
  "maxOcrPagesPerPdf": 50,
  "pageRange": "",
  "includePages": false,
  "chunkSize": 0,
  "chunkOverlap": 200,
  "maxFileSizeMb": 100,
  "downloadTimeoutSecs": 60,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "urls": [
        "https://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sumitr_mardy/pdf-text-extractor").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 = { "urls": [
        "https://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sumitr_mardy/pdf-text-extractor").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 '{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/f1040.pdf",
    "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf"
  ]
}' |
apify call sumitr_mardy/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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

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/FUEXiZxc5QQxuLXFU/builds/RXbK3KEcbxHgEe6VV/openapi.json
