# PDF Text Extractor & Change Monitor (`produkdigitalali/pdf-text-extractor-change-monitor`) Actor

Extract text, page content, metadata, and RAG-ready chunks from PDFs. Monitor content, page, and metadata changes across runs.

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

## Pricing

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

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 & Change Monitor

Extract searchable text, per-page content, metadata, hashes, and optional RAG-ready chunks from public PDFs. Turn on monitoring to detect exactly when a document is new, its text changes, pages are added/removed, or metadata changes.

This Actor is designed for recurring document intelligence rather than one-off PDF parsing: regulatory documents, product catalogs, tender PDFs, research reports, public policies, manuals, supplier price lists, and knowledge-base sources can be re-run on a schedule with stable change events.

### Key features

- Batch public PDF URLs plus an easy Apify file-upload input
- Full normalized text and optional per-page text
- PDF title/author/creator/producer/date metadata
- Stable document ID, file hash, content hash, metadata hash, and per-page hashes
- Optional page-aware RAG chunks with stable chunk IDs
- Detects likely scanned/textless PDFs without pretending OCR succeeded
- Password support for PDFs you are authorized to access
- Monitoring events: `NEW`, `CONTENT_CHANGED`, `PAGE_ADDED`, `PAGE_REMOVED`, `METADATA_CHANGED`, `UNCHANGED`
- Safe monitoring: failed/unavailable PDFs retain prior state and are never treated as removed
- Private/local network URLs are blocked

### Quick start

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "includeFullText": true,
  "includePageText": true,
  "includeChunks": false,
  "monitorMode": false
}
```

### Monitor PDF changes

Run the same document identity repeatedly using the same `monitorKey`:

```json
{
  "documents": [
    {
      "url": "https://example.com/regulation.pdf",
      "documentId": "regulation-2026",
      "label": "Regulation 2026"
    }
  ],
  "monitorMode": true,
  "emitChangesOnly": true,
  "monitorKey": "regulatory-watch"
}
```

A stable explicit `documentId` is recommended for signed URLs or URLs whose query string changes between runs.

### RAG-ready chunks

```json
{
  "pdfUrls": ["https://example.com/manual.pdf"],
  "includeChunks": true,
  "chunkSizeCharacters": 1800,
  "chunkOverlapCharacters": 200
}
```

Chunks are page-aware and include `chunkId`, `pageNumber`, `chunkIndex`, `text`, `textHash`, and `characterCount`.

### Important output fields

- `documentId`, `label`, `inputUrl`, `finalUrl`
- `pageCount`, `textPageCount`, `characterCount`
- `fullText`, `pageTexts[]`, `chunks[]`
- `metadata`
- `fileHash`, `contentHash`, `metadataHash`
- `likelyScanned`, `ocrRecommended`, `textTruncated`
- `changeType`, `changedPages`, `pageDiffs[]`, `addedLineCount`, `removedLineCount`
- `previousPageCount`, `previousContentHash`, `previousMetadataHash`, `previousFileHash`

### Monitoring semantics

- `NEW`: no previous state exists for this document identity
- `CONTENT_CHANGED`: one or more page text hashes changed while page count stayed the same; bounded line-level added/removed snippets are emitted when previous text is stored
- `PAGE_ADDED`: current PDF has more pages than the previous successful observation
- `PAGE_REMOVED`: current PDF has fewer pages than the previous successful observation
- `METADATA_CHANGED`: extracted text/pages are unchanged but PDF metadata changed
- `UNCHANGED`: meaningful monitored fields are unchanged

Monitoring state is stored per document rather than as one giant batch record, so adding PDFs to an existing monitor does not reset baselines and large batches do not create an oversized state record.

If a PDF download or parse fails, its previous state is preserved. A transient network problem will not be reported as a removed document.

### Scanned PDFs / OCR

Version 1 intentionally does not perform OCR. A PDF with no extractable text, or a document where most pages are textless and the remaining extracted text is sparse, is marked with `likelyScanned: true` and `ocrRecommended: true`. Short digital PDFs that still contain extractable text are not flagged just because they have a low character count. This keeps normal extraction fast, cheap, and predictable. OCR can be added later as a separately priced feature if demand justifies it.

### Pricing

This Actor is designed for transparent workload-based Pay Per Event pricing:

- **PDF megabyte processed:** `$0.0003` per started MiB (1,048,576 bytes) of each successfully processed PDF.
- **PDF page processed:** `$0.00005` per page of each successfully processed PDF.
- The standard Apify Actor-start event may also apply.

For example, a PDF smaller than 1 MiB with 3 pages costs about `$0.00045` in document-processing events. A batch of 1,000 similar PDFs is about `$0.45`, plus the small run-start event.

Why two workload units? File size tracks network-transfer cost while page count tracks extraction work, so small PDFs stay inexpensive without making very large or very long PDFs economically unsafe.

A successfully checked monitoring document is processed work even when `emitChangesOnly=true` suppresses an `UNCHANGED` dataset row. Failed PDFs are reported in `RUN_SUMMARY` and are not charged by these custom workload events.

### Performance sanity check

A controlled Apify Cloud benchmark using a 3-page public PDF with full text, per-page text, and RAG chunks enabled completed 10/50/100-document batches successfully. The 100-document batch produced 100 results / 300 pages / 700 chunks in about 15 seconds in that test. Real-world speed varies with PDF size, page count, origin-server latency, and concurrency.

### Responsible use

Process only documents you are authorized to access. The Actor works with public URLs or files supplied by the user and does not bypass authentication or access controls.

# Actor input Schema

## `uploadedPdf` (type: `string`):

Optional single PDF uploaded to an Apify key-value store or supplied by URL.

## `pdfUrls` (type: `array`):

Public http(s) PDF URLs to process. Add multiple URLs for batch extraction.

## `documents` (type: `array`):

Optional JSON objects with url, documentId and label. Use documentId to keep monitoring identity stable when signed/query URLs change.

## `maxDocuments` (type: `integer`):

Maximum number of unique PDFs processed in this run.

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

Include normalized text from all text-bearing pages in the result.

## `includePageText` (type: `boolean`):

Include text, character count and stable text hash for each page.

## `includeChunks` (type: `boolean`):

Create page-aware text chunks with stable chunk IDs and hashes for downstream RAG/vector pipelines.

## `chunkSizeCharacters` (type: `integer`):

Target maximum chunk length when RAG-ready chunks are enabled.

## `chunkOverlapCharacters` (type: `integer`):

Character overlap between adjacent chunks on the same page.

## `maxChunksPerPdf` (type: `integer`):

Safety cap for generated RAG chunks per document.

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

Optional password for encrypted PDFs. Stored as a secret input.

## `monitorMode` (type: `boolean`):

Compare each successfully processed PDF with persistent state from previous runs using the same monitor key and document identity.

## `emitChangesOnly` (type: `boolean`):

When monitoring, omit UNCHANGED documents from the default dataset.

## `monitorKey` (type: `string`):

Namespace for persistent monitoring state. Keep it unchanged across related runs.

## `storeTextForDiff` (type: `boolean`):

Stores bounded per-page text in persistent monitoring state so changed pages can include added/removed line snippets.

## `maxStateCharactersPerPdf` (type: `integer`):

Maximum characters retained per PDF in persistent state for future line-level diffs.

## `maxDiffLinesPerPage` (type: `integer`):

Caps added/removed line snippets emitted for each changed page while preserving total line counts.

## `removeTrackingParams` (type: `boolean`):

Removes common utm/fbclid/gclid parameters when deriving stable IDs. The original URL is still downloaded unchanged.

## `concurrency` (type: `integer`):

Maximum PDFs downloaded/processed concurrently.

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

Per-document download safety limit.

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

Rejects PDFs above this page count to keep runtime predictable.

## `maxExtractedCharacters` (type: `integer`):

Safety cap for extracted text stored per PDF. The result marks textTruncated when reached.

## `requestTimeoutSeconds` (type: `integer`):

Timeout for downloading each PDF.

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

Retries transient network/429/5xx failures before recording a document failure.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "documents": [],
  "maxDocuments": 100,
  "includeFullText": true,
  "includePageText": true,
  "includeChunks": false,
  "chunkSizeCharacters": 1800,
  "chunkOverlapCharacters": 200,
  "maxChunksPerPdf": 5000,
  "monitorMode": false,
  "emitChangesOnly": false,
  "monitorKey": "default",
  "storeTextForDiff": true,
  "maxStateCharactersPerPdf": 500000,
  "maxDiffLinesPerPage": 50,
  "removeTrackingParams": true,
  "concurrency": 5,
  "maxFileSizeMb": 50,
  "maxPagesPerPdf": 2000,
  "maxExtractedCharacters": 2000000,
  "requestTimeoutSeconds": 30,
  "maxRetries": 2
}
```

# Actor output Schema

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

One structured dataset item per successfully processed PDF. With emitChangesOnly enabled, unchanged PDFs are omitted.

## `runSummary` (type: `string`):

Counts, monitoring diagnostics and errors for PDFs that could not be processed.

# 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 = {
    "pdfUrls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("produkdigitalali/pdf-text-extractor-change-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 = { "pdfUrls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,produkdigitalali/pdf-text-extractor-change-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/zccgxujo9y7hfJ46z/builds/NyqvTzAMetr2527ja/openapi.json
