# PDF to Text & Markdown Extractor with Page Hashes (`plainkitbin/pdf-extract`) Actor

Extracts clean text and Markdown from 1 to 5 public PDF URLs, page by page, with deterministic SHA-256 page hashes and batch manifests. Automatic contact scrubbing protects privacy. Deterministic extraction designed for document ingestion and LLM embedding pipelines.

- **URL**: https://apify.com/plainkitbin/pdf-extract.md
- **Developed by:** [PlainKit bin](https://apify.com/plainkitbin) (community)
- **Categories:** Developer tools, AI, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.33 / 1,000 pdf page processeds

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 to Text & Markdown Extractor with Page Hashes

### Introduction

This Actor takes public PDF URLs and returns the text and Markdown of each page, with SHA-256 hashes for each page and for the whole document. Operating as a deterministic extractor, it downloads public PDF documents from web URLs, extracts page text using `pypdf`, converts each page into clean Markdown, and computes deterministic SHA-256 hashes for each page and the overall document manifest from the normalized, scrubbed text and Markdown.

Scanned or image-only PDFs give empty text for their pages, because this Actor has no OCR.

#### Use cases

- **Document ingestion pipelines**: Convert technical manuals, legal filings, and whitepapers into structured page-by-page Markdown for LLM embedding.
- **Pipeline change tracking**: Track deterministic SHA-256 page hashes of normalized, scrubbed text and Markdown across batch runs to identify identical extracted pages.
- **Batch text extraction**: Extract clean text from multiple public PDF URLs with bounded page, memory, and timeout limits.

### Tutorial

#### Step-by-step guide

1. Prepare a JSON input object containing `pdf_urls` with 1 to 5 public HTTP or HTTPS PDF links.
2. Run the Actor on the Apify platform or start a run via the Apify API.
3. The Actor verifies `robots.txt` compliance, downloads the PDF files within size limits, parses text page-by-page, scrubs contact details, and computes hashes.
4. Export the resulting batch summary, page Markdown, and integrity manifests in JSON, CSV, or Excel format.

### Pricing

The Actor is priced per extracted page under the Pay-Per-Event model using the **`pdf-page`** event.

| Usage tier | Price per `pdf-page` event |
| --- | ---: |
| FREE | $0.002 |
| BRONZE | $0.0018 |
| SILVER | $0.0016 |
| GOLD, PLATINUM, DIAMOND | $0.001333 |

#### Charge contract

Each row is pushed first and charged right after a successful push; a refused charge stops work and that row stays delivered and uncharged; a reached limit stops work after that row was charged; failed items are neither pushed nor charged. Documents blocked by `robots.txt`, download failures, or malformed PDF streams produce no billable page items and are not charged.

- **Restarted runs**: Pages already in the dataset of a restarted run are skipped and not charged again; the run reads the dataset first, and if it cannot be read or a row is malformed, the run fails with DATASET_READ_FAILED before any new row or charge. On restarted runs, skipped pages are excluded from the summary manifest, which covers only newly delivered pages. The restart tests prove that a rerun on a dataset holding pages 1 and 2 of a 3-page document pushes and charges only page 3 (RESTART_NO_DOUBLE_CHARGE=PASS), and that a raising read, a non-list `documents`, a malformed page row, a non-object row, or a non-object page entry each end the run as failed with no push, no charge and no fetch (DATASET_READ_FAIL_STOPS=PASS).
- **Largest bill**: For the largest input the limits allow, 5 documents of 200 pages each (1,000 pages), the bill is $2.00 at the FREE price ($0.002 per page) and $1.333 at the GOLD, PLATINUM or DIAMOND price ($0.001333 per page). The byte limits can make this input unreachable, so it is an upper bound.

#### Operational limits

- Accepts 1 to 5 PDF document URLs per run (`pdf_urls`).
- Document size is capped at 25,000,000 bytes per document and 50,000,000 bytes across the run.
- Up to 200 pages per document; maximum 2,000,000 extracted text characters per page.
- URL length is limited to 2,048 characters with `.pdf` suffix required.
- Requests have a 20-second socket timeout and follow at most 5 redirects.
- Evaluates `robots.txt` rules before fetching; disallowed documents are skipped safely.

### Input and output examples

#### Input example

```json
{
  "pdf_urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}
```

#### Output example

```json
{
  "status": "SUCCESS",
  "documents": [
    {
      "source_id": "3d7d1b17f7597eedac3f418a",
      "source_kind": "pdf_url",
      "status": "SUCCESS",
      "pages": [
        {
          "page": 1,
          "text": "Dummy PDF file",
          "markdown": "# Page 1\n\nDummy PDF file\n",
          "text_sha256": "4a75ab26b9047bbb1e89e8c47545aa668a01a3186e8ed8e629518c3aab6576d9",
          "markdown_sha256": "4fe62875fdd288a552d83dfda89bdfcb84774cea3d8b808237c7b5a35cf25fb0",
          "page_sha256": "c90d847510847ac327f52ec66360e7ad0a41bbe58c689bf3d8b0f75394548755"
        }
      ],
      "limit_reached": false,
      "integrity": {
        "page_hashes": [
          "c90d847510847ac327f52ec66360e7ad0a41bbe58c689bf3d8b0f75394548755"
        ],
        "manifest_sha256": "62937d4d23c019f65b55871066286bd48f898d28a006f801cae8bbc2de77389c"
      }
    }
  ],
  "total_bytes": 13264,
  "integrity": {
    "document_manifests": [
      "62937d4d23c019f65b55871066286bd48f898d28a006f801cae8bbc2de77389c"
    ],
    "manifest_sha256": "98b620bc678c7cd8d89fe8b80414e120e0f7521c643c61a7ccd21fef19a9d11b"
  }
}
```

#### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `status` | String | Overall batch execution status code (`SUCCESS`) |
| `documents` | Array | Array of document result objects |
| `documents[].pages` | Array | Array of extracted page objects with text and Markdown |
| `documents[].pages[].page` | Integer | 1-based page number within the document |
| `documents[].pages[].source_id` | String | Source identity; with `page`, it is the key of one page for restarted runs. Each page is also pushed on its own as a row with `source_id` and `page` before it is charged |
| `documents[].pages[].markdown` | String | Formatted Markdown text of the page |
| `documents[].pages[].page_sha256` | String | SHA-256 hash computed from page number, scrubbed text hash, and scrubbed markdown hash |
| `documents[].integrity.manifest_sha256` | String | SHA-256 hash of page hashes delivered in this run for the document |
| `total_bytes` | Integer | Total bytes processed across all documents |
| `integrity.manifest_sha256` | String | Master SHA-256 hash across document manifests delivered in this run |

### Actor recommendations

Pair this Actor with:

- **CSV and JSON Integrity Parser**: Validate tabular data and compare integrity manifests alongside PDF data.
- **Inline HTML and Markdown to Clean Markdown**: Clean and normalize extracted PDF markdown for downstream publishers.
- **Readable Article Extractor**: When source content is available as HTML web articles rather than static PDF files.

### FAQ and support

#### Does the Actor perform OCR on scanned PDF pages?

No. The Actor extracts digital text streams directly using `pypdf`. Image-only or scanned PDFs produce empty text for those pages.

#### How does the Actor ensure data privacy?

Contact information (including email addresses and phone numbers) in extracted text is scrubbed and replaced with `[email removed]` and `[phone removed]`. Page hashes (`text_sha256`, `markdown_sha256`, `page_sha256`) are computed after contact scrubbing and text normalization.

#### Are failed pages charged?

No. A timeout, robots disallow, or extraction failure leaves that page unpushed and uncharged.

#### Support and inquiries

For technical support, questions, or issues, submit a ticket in the Apify Console.

# Actor input Schema

## `pdf_urls` (type: `array`):

HTTP or HTTPS URLs of PDF files, with up to five URLs.

## Actor input object example

```json
{
  "pdf_urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}
```

# Actor output Schema

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

Dataset items produced by this run.

# 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 = {
    "pdf_urls": [
        "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("plainkitbin/pdf-extract").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 = { "pdf_urls": ["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("plainkitbin/pdf-extract").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 '{
  "pdf_urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call plainkitbin/pdf-extract --silent --output-dataset

```

## MCP server setup

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

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/EMIHzBremRs0GuxLy/builds/rCtcTfCFJdT0FNujn/openapi.json
