# PDF Text Extractor: PDF to Text, Pages & OCR (`adamhaleworks/pdf-text-extractor-ocr`) Actor

Paste PDF links, get clean text, per-page text, RAG-ready chunks and metadata (title, author, dates, page count). Scanned pages are read with Tesseract OCR in 7 languages. Failed, encrypted and empty files are free. Free while in launch; planned price $2 per 1,000 PDFs.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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: PDF to text, per-page text, RAG chunks and OCR for scanned pages

Paste links to PDF files and get **clean text and metadata for each one**: full text, page count, word count, title, author, subject, keywords, creation and modification dates, PDF version. Pages without a text layer (scans, photographed documents) are **read with Tesseract OCR** in English, German, French, Spanish, Italian, Portuguese or Dutch.

- **One row per PDF or one row per page**, with optional **chunks for RAG** (cut at paragraph, sentence or word boundaries, with overlap).
- **OCR only where needed**: pages with embedded text are read directly; only pages with (almost) no text are rendered at 300 dpi and OCR-read.
- **Fast**: PDFium text extraction, no browser. A 15-page paper takes well under a second after download.
- **Free while in launch; planned price $2 per 1,000 PDFs plus $3 per 1,000 OCR pages** (pay per event). Files that fail to download, are not PDFs, are encrypted or contain no text would not be charged.

### What can I use it for?

- **RAG and AI pipelines**: turn reports, papers, manuals and contracts into chunks for a vector database or an LLM prompt.
- **Document search and archiving**: index the text of PDFs linked from your site, a data portal or a list of filings.
- **Research**: bulk-extract text from papers (arXiv, journals), government publications and annual reports.
- **Scanned documents**: get text out of scanned letters, forms and invoices that have no text layer.
- **Metadata audits**: list titles, authors, producers and dates of many PDFs at once.

### What data does it return?

One row per PDF (default):

| Field | Description |
|---|---|
| `url`, `finalUrl`, `status`, `httpStatus` | What was requested and what happened. `status` is `ok` or why the file was not read |
| `text`, `textLength`, `wordCount` | Full text, pages separated by a blank line |
| `pageCount`, `pagesExtracted`, `pagesOcr`, `pagesWithoutText` | Page statistics |
| `title`, `author`, `subject`, `keywords`, `creator`, `producer` | Document information from the PDF |
| `creationDate`, `modificationDate`, `pdfVersion`, `fileSizeBytes` | ISO 8601 dates (with the PDF's time zone), version, size |
| `pages` | `[{page, text, ocr}]`, with **Add per-page texts** on |
| `chunks` | Text pieces of at most **Chunk size** characters, when set |
| `warning`, `error`, `fetchedAt` | Page limit reached, OCR failures, or why the file failed |

In **One row per page** mode each row has `page`, `pageText`, `pageTextLength`, `pageOcr` (and `pageChunks`), plus the PDF's metadata.

Files that could not be read get a row with `status` `not_pdf`, `encrypted`, `parse_error`, `no_text`, `too_large`, `http_error`, `blocked_by_site`, `disallowed_by_robots_txt`, `robots_txt_unreachable`, `unreachable`, `timeout`, `skipped` or `invalid_url`, and a readable `error`. `SUMMARY` in the key-value store counts PDFs, OCR pages and statuses.

#### Example output

From a real run on 2026-10-12 (text shortened):

```json
{
  "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "status": "ok",
  "httpStatus": 200,
  "fileSizeBytes": 140815,
  "pageCount": 6,
  "pagesExtracted": 6,
  "pagesOcr": 0,
  "pagesWithoutText": 0,
  "textLength": 37689,
  "wordCount": 6516,
  "title": "Form W-9 (Rev. March 2024)",
  "subject": "Request for Taxpayer Identification Number and Certification",
  "keywords": "Fillable",
  "producer": "Designer 6.5",
  "creationDate": "2024-03-06T08:18:13-05:00",
  "pdfVersion": "1.7",
  "text": "Form W-9\n(Rev. March 2024)\nRequest for Taxpayer\nIdentification Number and Certification\nDepartment of the Treasury\nInternal Revenue Service …"
}
```

A scanned page without a text layer (OCRmyPDF's `skew.pdf` test file, run on Apify on 2026-10-12) came back with `"pagesOcr": 1`, 740 words, in 8 seconds. It starts `"The LinnSequencer\n\n32 Track MIDIS\n\nequence Recorder\n\nThe LinnSequencer is a state-of-the-art composition and performance tool…"`: body text is read well, while a skewed heading split oddly.

### How much does it cost?

**Free while in launch.** The actor charges no fee of its own right now; you pay only the Apify platform usage of your runs (the free Apify plan includes $5 of usage a month). The prices below are planned and apply only once they are switched on; Apify notifies existing users before a price change takes effect.

| Event | Planned price |
|---|---|
| PDF read (any number of pages up to the page limit) | $0.002 (= **$2 per 1,000 PDFs**) |
| Scanned page read with OCR that returned text | $0.003 (= $3 per 1,000 pages) |

At the planned prices, the default input (3 PDFs, 30 pages, no scans) would cost $0.006. **Not charged:** failed downloads, non-PDF files, encrypted or broken PDFs, PDFs without any text, OCR that finds nothing, invalid URLs. Once pricing is on, **Maximum cost per run** stops the run cleanly.

For comparison (Apify Store, checked 2026-10-12), the three most-used PDF text extractors charge $3.45 per 1,000 PDFs plus $0.005 per run; $5 per 1,000 PDFs plus $15 per 1,000 OCR pages and $0.005 per run; and $20 per 1,000 PDFs plus $1 per 1,000 pages.

### How to use it

1. Paste direct PDF links into **PDF URLs** (up to 10,000).
2. Choose **One row per PDF** or **One row per page**, optionally a **Chunk size** for RAG.
3. Leave **OCR scanned pages** on and pick the documents' languages.
4. Press Start, then export JSON, CSV or Excel, or call it from your pipeline through the API.

#### Input example

```json
{
  "urls": ["https://arxiv.org/pdf/1706.03762", "https://bitcoin.org/bitcoin.pdf"],
  "outputMode": "document",
  "chunkSize": 1500,
  "chunkOverlap": 200,
  "ocr": true,
  "ocrLanguages": ["eng", "deu"]
}
```

#### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~pdf-text-extractor-ocr/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://bitcoin.org/bitcoin.pdf"], "chunkSize": 1000}'
```

### How it works

- Text comes from PDFium (the PDF engine in Chrome), through `pypdfium2`. Reading order follows the PDF's text objects; multi-column layouts usually come out column by column.
- A page with fewer than 20 non-space characters is treated as scanned. With OCR on it is rendered at 300 dpi in grayscale and read with Tesseract 5.
- Downloads use one honest User-Agent, `pdf-text-extractor-ocr/1.0 (+https://apify.com/<this actor>)`, respect robots.txt, send at most 2 requests at a time per host and do not use proxies or get around bot protection.

### Limitations

- Tables come out as text lines, not cells. Markdown headings are not generated.
- Password-protected PDFs are reported as `encrypted` and skipped.
- Files must be reachable by URL; private and internal addresses are refused. The maximum size is 200 MB, and 50 MB by default.
- OCR quality depends on the scan; handwriting is not supported.

### Is it legal?

The actor downloads only the files you list and follows each site's robots.txt. The text stays the copyright of its owners; you are responsible for having the right to process the documents you submit.

### FAQ

**Why is a PDF `no_text`?** It has no text layer and OCR was off, or OCR found nothing (for example a blank or image-only page without letters). It is not charged.

**Can I upload a PDF instead of a URL?** Not directly. Put it anywhere that gives a public download link that works without a login, and paste that link.

**Something is wrong or missing?** Open an issue on the actor's Issues tab and include the PDF URL.

***

Author: Adam Hale

# Actor input Schema

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

Direct links to PDF files, one per line. Up to 10,000 per run.

## `outputMode` (type: `string`):

One row per PDF (full text in 'text'), or one row per page (text in 'pageText', with the PDF's metadata repeated).

## `includePageTexts` (type: `boolean`):

Adds 'pages': \[{page, text, ocr}] in 'One row per PDF' mode.

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

0 = off. Otherwise adds 'chunks' (or 'pageChunks' per page): pieces of at most this many characters, cut at paragraph, sentence or word boundaries.

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

Characters repeated between consecutive chunks (at most half the chunk size).

## `ocr` (type: `boolean`):

Pages with fewer than 20 characters of embedded text are rendered at 300 dpi and read with Tesseract OCR.

## `ocrLanguages` (type: `array`):

Languages in the scanned documents. More languages are slower.

## `maxPagesPerPdf` (type: `integer`):

Pages after this are skipped (the row says so).

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

Larger files are reported as 'too_large' and not charged.

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

PDFs downloaded and read at the same time. At most 2 requests run against the same host.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each download.

## `maxRetries` (type: `integer`):

Retries for network errors and HTTP 429/5xx answers, with backoff. Retry-After is honoured up to 30 s.

## Actor input object example

```json
{
  "urls": [
    "https://bitcoin.org/bitcoin.pdf",
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputMode": "document",
  "includePageTexts": false,
  "chunkSize": 0,
  "chunkOverlap": 200,
  "ocr": true,
  "ocrLanguages": [
    "eng"
  ],
  "maxPagesPerPdf": 500,
  "maxFileSizeMb": 50,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 60,
  "maxRetries": 2
}
```

# Actor output Schema

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

Dataset items, one per PDF or per page. Views: overview and pages.

## `summary` (type: `string`):

SUMMARY record in the default key-value store: PDFs requested, read and charged, OCR pages, status counts, stop reason.

# 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://bitcoin.org/bitcoin.pdf",
        "https://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adamhaleworks/pdf-text-extractor-ocr").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://bitcoin.org/bitcoin.pdf",
        "https://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("adamhaleworks/pdf-text-extractor-ocr").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://bitcoin.org/bitcoin.pdf",
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call adamhaleworks/pdf-text-extractor-ocr --silent --output-dataset

```

## MCP server setup

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

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/wVNsGGgDKihZZftnD/builds/uPnHefg6y9LQuMryd/openapi.json
