# PDF Text Extractor - Watch PDFs and Get Only What Changed (`neverempty/pdf-text-extractor-monitor`) Actor

For watching published price lists, tariffs, timetables and terms, and for feeding RAG pipelines: PDF links in, text out, one row per PDF with page count, title, author and dates. The IRS W-9 gave 21,855 characters on 2026-09-07. Watch the same URLs and a PDF returns only when its text changed.

- **URL**: https://apify.com/neverempty/pdf-text-extractor-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (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

from $3.65 / 1,000 pdf text 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF Text Extractor — watch PDFs and get only what changed

**For watching a published price list, a timetable, a set of terms, a tariff or a regulatory document**: give it PDF links and it returns the text, the page count, the title, the author and the dates, one row per PDF — and it can **watch the same PDFs** and return one only when its text has actually changed.

**Measured on 2026-09-07:** IRS Form W-9 (`irs.gov/pub/irs-pdf/fw9.pdf`) — **6** pages, **21,855 characters** from the first 3. *Attention Is All You Need* (`arxiv.org/pdf/1706.03762`) — **15** pages, **9,175 characters** from the first 3.

**A daily watch costs nothing on the days nothing moved**, because unchanged PDFs are neither returned nor charged. You are charged per PDF that actually produced text: a 404, a 403, a password-protected file, a URL that answers with something that is not a PDF, and a file over the size limit each come back as their own row with the reason, and none of them are billed.

Take a page range (`1-5`, `2`, `1-3,7`) or the whole file, and optionally drop repeated headers and footers — the lines that appear on 70% or more of the pages, which is what a running header is. No API key. Export as JSON, CSV or Excel.

### What it does not do

- **It does not read scanned paper.** A PDF that is only images has no text layer. This Actor reports that as
  `no-text`, with the page count and the reason, and **does not charge for it**. It does not guess, and it does
  not return an empty document as if the PDF were blank.
- It does not do OCR, and it does not render pages to images.

### Measured on 2026-09-07

| PDF | Pages | Text |
|---|---|---|
| IRS Form W-9 (`irs.gov/pub/irs-pdf/fw9.pdf`) | 6 | 21,855 characters from the first 3 pages |
| *Attention Is All You Need* (`arxiv.org/pdf/1706.03762`) | 15 | 9,175 characters from the first 3 pages |
| W3C `dummy.pdf` | 1 | 15 characters |

Each of those took between 0.1 and 1.4 seconds to download and read.

Four failure paths were checked against live URLs and each returns a reason instead of a blank row:
HTTP 404, HTTP 403, a URL that answers with something that is not a PDF, and a file over the size limit.

### Monitoring mode

Turn on **Monitoring mode** and the Actor remembers the text of every PDF it returned.

- The **first run** returns every PDF and remembers it.
- Every run after that returns a PDF **only if its text is different**.
- Each returned row carries `previousContentHash`, `previousCharCount` and `charCountDelta`, so you can see how
  much of the document moved without diffing it yourself.
- `isFirstCheck` says whether this URL had ever been seen before.
- When nothing has changed, one row explains that and **nothing is charged**.

The key is the URL plus a hash of the extracted text. Re-uploading the same document under a new file name is
treated as a new document; editing a document in place is treated as a change.

### What comes back

One row per PDF:

| Field | What it is |
|---|---|
| `url`, `finalUrl`, `httpStatus` | The link you gave, where it ended up after redirects, and the status |
| `numPages`, `pagesExtracted` | How many pages the PDF has, and how many were read (page range / limits) |
| `title`, `author`, `subject`, `keywords`, `creator`, `producer` | The PDF's own metadata, as it stores it |
| `createdAt`, `modifiedAt` | The dates inside the PDF, converted to ISO. `null` when the PDF does not carry them |
| `pdfVersion`, `isEncrypted`, `sizeBytes` | The file itself |
| `charCount`, `wordCount` | Size of the extracted text |
| `text` | The whole document as one string |
| `pages` | Per-page text, when you ask for it |
| `contentHash` | A short hash of the text — the thing monitoring compares |
| `previousContentHash`, `previousCharCount`, `charCountDelta`, `isFirstCheck` | Filled in when monitoring |
| `status`, `reason` | `ok`, or why this PDF could not be turned into text |

A field the PDF does not state comes back as `null`. Nothing is filled in with a zero or an empty string.

### Page range, limits and filters

- **Page range** — `1-5`, `2`, or `1-3,7`. Empty means every page.
- **Maximum pages per PDF** — stop after N pages of each document.
- **Maximum PDF size (MB)** — a larger file is reported with its real size instead of being downloaded in full.
- **Remove repeated headers and footers** — drops lines that appear on 70% or more of the pages, which is what a
  running header or footer looks like. It only applies from 3 pages up, because below that there is nothing to
  compare against.
- **Keywords / Exclude keywords / Minimum characters** — filter the documents you already fetched. Nothing extra
  is downloaded to apply them.

### Pricing

Charged per PDF that actually produced text. A PDF that could not be read — a 404, a 403, a password-protected
file, a scanned document with no text layer, or a file over your size limit — is returned with the reason and is
**not charged**.

### Notes

- Passwords go in `pdfPassword`. A protected PDF without one is reported as `password-required`.
- The PDF is identified by its `%PDF-` header, not by the file extension, so a URL without `.pdf` still works and
  a `.pdf` URL that serves an error page is reported as `not-a-pdf`.
- Downloads run in parallel; set **Max concurrency** to be gentler on a server that hosts many of your files.

# Actor input Schema

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

Direct links to the PDF files you want read. Anything that is not a PDF is reported as such rather than returned as empty text. If this and the paste box below are both left empty, two example PDFs (IRS Form W-9 and arXiv 1706.03762) are read and a free row says so. If the two example PDFs are still here when you paste your own URLs below, they are skipped and not charged.

## `urlsText` (type: `string`):

The same thing, as a block you can paste. Both fields are merged and duplicates are dropped.

## `pdfPassword` (type: `string`):

Password for protected PDFs. A protected PDF with no password is reported as password-required and is not charged.

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

Which pages to read, for example 1-5 or 2 or 1-3,7. Empty means every page.

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

Stop after this many pages in each PDF. 0 = no limit.

## `maxPdfs` (type: `integer`):

How many of the given URLs to read in this run.

## `maxPdfSizeMb` (type: `integer`):

A PDF larger than this is reported with its actual size instead of being downloaded in full.

## `timeoutPerPdfSecs` (type: `integer`):

Give up on a single PDF after this many seconds. The other PDFs in the run are still returned.

## `includeFullText` (type: `boolean`):

Return the whole document as one text field.

## `includePerPageText` (type: `boolean`):

Also return an array with the text of each page separately.

## `removeRepeatedLines` (type: `boolean`):

Drops lines that appear on 70% or more of the pages, which is what a running header or footer looks like. Only applies to documents of 3 pages or more.

## `minCharCount` (type: `integer`):

Skip a PDF whose extracted text is shorter than this. 0 = keep everything.

## `keywords` (type: `array`):

Keep only PDFs whose text, title, subject or author contains these. Empty = keep everything.

## `keywordMatch` (type: `string`):

any = contains at least one of them. all = contains every one of them.

## `excludeKeywords` (type: `array`):

Drop a PDF if its text contains any of these.

## `monitoringMode` (type: `boolean`):

Reads the same URLs again and returns a PDF only when its text is different from the last run. The first run returns everything and remembers it. Unchanged PDFs are not returned and not charged, so a daily watch on a price list, a timetable or a set of terms costs nothing on the days nothing moved.

## `resetMonitoringState` (type: `boolean`):

Clears the memory so the next monitoring run treats every PDF as new.

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

How many PDFs to download at the same time.

## Actor input object example

```json
{
  "urls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    "https://arxiv.org/pdf/1706.03762"
  ],
  "urlsText": "",
  "pdfPassword": "",
  "pageRange": "",
  "maxPagesPerPdf": 0,
  "maxPdfs": 25,
  "maxPdfSizeMb": 50,
  "timeoutPerPdfSecs": 30,
  "includeFullText": true,
  "includePerPageText": false,
  "removeRepeatedLines": true,
  "minCharCount": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Every PDF this run turned into text, with the metadata and the content hash.

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

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

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

```

## MCP server setup

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

```

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/FejC8b3XfYHaglqWQ/builds/ZIPrkcyiCoPCHtnkf/openapi.json
