# PDF Text Extractor — Per Page, No OCR (`hourwright/pdf-text-extractor`) Actor

Extract clean text from PDF URLs, page by page. Pay only for pages that have text: empty pages, broken files and failed downloads are free. Respects robots.txt.

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

## Pricing

$2.00 / 1,000 page extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 — Per Page, No OCR

Turn PDF links into clean text, **one dataset item per page**. Built for RAG pipelines, search indexes, and
spreadsheets fed from Make, n8n, Zapier or the Apify API.

**You pay only for pages that have text: $2.00 per 1,000 pages.** Empty pages (scanned images), broken or
password-protected files, failed downloads, URLs that are not PDFs and URLs that `robots.txt` forbids are reported
in the dataset and are **never charged**. Invalid input fails the run at once, before anything is charged.

*[Leer en castellano](README.es.md)*

### What you get

- The text of every page, cleaned up (Unicode-normalized, no control characters, collapsed whitespace), with its
  page number, the document's page count and its title.
- A clear status for everything that was skipped, in plain words, in a separate **Skipped (not charged)** view.
- With **Detect changes**, a fingerprint of each document is kept between runs. Pages are flagged when their
  document changed, and you get a warning when a URL that used to return a PDF now returns something else (a moved
  report, a login page…). Run it on a schedule to watch documents.

### Input

| Field | What it does | Default |
|---|---|---|
| `urls` | Direct links to PDF files (up to 1,000 per run) | — |
| `maxPagesPerDocument` | Only the first N pages of each PDF (0 = all) | 0 |
| `maxFileSizeMb` | Larger files are skipped, free | 50 |
| `includeEmptyPages` | Also list pages with no text layer (never charged) | false |
| `detectChanges` | Remember documents between runs and flag changes | false |
| `maxConcurrency` | Documents at the same time; one request at a time per site | 4 |

```json
{
  "urls": [
    { "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" },
    { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }
  ]
}
```

### Output

A real run on 19 October 2026 with the two URLs above plus the IRS *Publication 17* gave 149 pages with text from
3 documents in about 15 seconds. Two of the items (text shortened here):

```json
{
  "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "status": "ok", "page": 1, "pageCount": 1,
  "text": "Dummy PDF file",
  "charCount": 14, "wordCount": 3, "isEmpty": false, "title": null,
  "changedSinceLastRun": null,
  "sha256": "3df79d34abbca99308e79cb94461c1893582604d68329a41fd4bec1885e6adb4", "message": null
}
{
  "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "status": "ok", "page": 1, "pageCount": 6,
  "text": "Form W-9\n(Rev. March 2024)\nRequest for Taxpayer\nIdentification Number and Certification\n…",
  "charCount": 5659, "wordCount": 933, "isEmpty": false, "title": "Form W-9 (Rev. March 2024)",
  "changedSinceLastRun": null,
  "sha256": "2d420cbb4123dcf1fb82595b2359cfbb5d81f00b9df9d359fcc7af361d093f53", "message": null
}
```

A skipped document looks like this (and costs nothing):

```json
{ "url": "https://example.com/report.pdf", "status": "source_changed", "page": null,
  "message": "this URL used to return a PDF and now returns «text/html» (starts with «<!doctype html>…»). The source has changed or moved; nothing was charged" }
```

Statuses: `ok`, `empty_page`, `robots_disallowed`, `robots_unavailable`, `download_failed`, `too_large`,
`not_a_pdf`, `source_changed`, `encrypted`, `broken_pdf`, `skipped_charge_limit`.

### Pricing

| Event | Price |
|---|---|
| Page with text extracted | $2.00 per 1,000 |

That's all. No start fee, no charge per result row. If you set a maximum cost per run, the Actor stops cleanly when
it's reached and tells you so in the run's status message.

### Good to know

- **No OCR.** Scanned PDFs have no text layer: their pages come out as `empty_page` (if you ask for them) and are free.
- **It's polite.** It reads each site's `robots.txt` first and skips what it forbids (or everything, if
  `robots.txt` answers with a server error). It makes one request at a time per site, with a pause, and retries
  timeouts, 429 and 5xx with backoff (honouring `Retry-After`). Its user agent is `HourwrightPdfTextExtractor`.
- **Your documents, your responsibility.** Use it on your own files or on documents that are public. It doesn't log
  in anywhere, bypass protections, or look for information about people.
- Questions or a document that comes out wrong? Open an issue on the Actor's page.

*By Hourwright.*

# Actor input Schema

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

Direct links to PDF files (up to 1,000 per run). Each URL is checked against the site's robots.txt before it is downloaded.

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

Only the first N pages of each PDF. 0 = all pages.

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

Larger files are skipped (and not charged).

## `includeEmptyPages` (type: `boolean`):

Also output pages without a text layer (e.g. scanned images), marked as empty_page. They are never charged.

## `detectChanges` (type: `boolean`):

Remember each document's fingerprint and flag pages whose document changed, or URLs that stopped returning a PDF. Useful on a schedule.

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

Documents processed at the same time. Requests to the same site always go one by one.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ],
  "maxPagesPerDocument": 0,
  "maxFileSizeMb": 50,
  "includeEmptyPages": false,
  "detectChanges": false,
  "maxConcurrency": 4
}
```

# Actor output Schema

## `pages` (type: `string`):

One item per PDF page, with its text, plus the files that could not be read and why.

# 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": [
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hourwright/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": [
        { "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" },
        { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("hourwright/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": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ]
}' |
apify call hourwright/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hourwright/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/4O23AteetIVBz2BuA/builds/tAUs7QrRox6bffsA5/openapi.json
