# PDF & HTML Text Extractor for RAG — $0.0005/page (`plainapi/pdf-html-document-text-extractor-for-rag`) Actor

You pay only for pages actually extracted. Nothing extracted, nothing charged. Turn PDF, HTML and text documents into clean plain text, optionally chunked with page numbers for retrieval. Ligatures expanded, hyphenation repaired, markup removed. You supply the URLs.

- **URL**: https://apify.com/plainapi/pdf-html-document-text-extractor-for-rag.md
- **Developed by:** [Shun Furu](https://apify.com/plainapi) (community)
- **Categories:** AI, Agents, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 text rows

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 & Document Text Extractor — for RAG

Turn PDFs, HTML pages and text files into **clean plain text**, optionally chunked with page
numbers, ready to embed.

```
Input:   ["https://arxiv.org/pdf/1706.03762"]
Output:  one row per page — or per chunk — with the page number kept
```

**You supply the URLs.** This Actor never decides on its own what to fetch, so it reads only the
documents you point it at.

### What it fixes

Getting bytes out of a PDF is easy. What ruins a retrieval pipeline is everything that comes with
them, and this handles each:

| Problem | What happens without it | Here |
|---|---|---|
| **Ligatures** | `ﬁle` is one character, not `file`. Exact search silently misses it | Expanded to real letters |
| **Hyphenation** | A word split across two lines becomes `re-` + `trieval`, two tokens that mean nothing | Rejoined |
| **Positioned fragments** | PDFs store text as placed fragments, not lines. Naive extraction joins them with spaces and destroys every paragraph | Line and paragraph breaks inferred from vertical position |
| **Invisible characters** | Soft hyphens and zero-width spaces break matching in ways nobody can see | Removed |
| **Smart punctuation** | Curly quotes and en-dashes stop exact matches working | Normalised |
| **Page numbers lost** | Chunk first and you can no longer cite a page | Page kept on every row and every chunk |
| **Markup in HTML** | Nav, scripts and styling end up in the text | Stripped, along with headers, footers and asides |

### Chunking

Set `chunkSize` and the text is split on paragraph boundaries with an overlap, so a sentence is
never cut in half and each chunk still carries the page it came from.

```json
{
  "urls": ["https://arxiv.org/pdf/1706.03762"],
  "chunkSize": 1000,
  "chunkOverlap": 200
}
```

Leave `chunkSize` at `0` and you get one row per page instead — useful when you want to chunk with
your own splitter but still want clean text and page numbers.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `urls` | array of strings | one example paper | URLs of PDF, HTML or text documents |
| `chunkSize` | integer | `0` | Characters per chunk. `0` returns one row per page |
| `chunkOverlap` | integer | `200` | Characters repeated between consecutive chunks |
| `maxPages` | integer | `0` (all) | Cap pages read per PDF |
| `keyword` | string | — | Keep only pages or chunks containing any of these words |

### Output

| Field | Type | Description |
|---|---|---|
| `url` | string | The document, after redirects |
| `kind` | string | `pdf` | `html` | `text` |
| `title` | string | PDF metadata title, or the HTML title |
| `author` | string | From PDF metadata where present |
| `page` | number | null | Page number for PDFs; null for HTML and text |
| `totalPages` | number | null | Pages in the document |
| `chunk` | number | null | Chunk index, when chunking is on |
| `text` | string | The cleaned text |
| `characters` | number | Length, so you can size embedding batches |
| `fetchedAt` | string | When the row was produced |

A `SUMMARY` record lists every document, what it resolved to, and how many rows it produced.

### Pricing

| | Price |
|---|---|
| Starting a run | **$0.00001** — one US cent per 1,000 runs |
| Each row returned | **$0.0005** — 50 US cents per 1,000 pages or chunks |

A document that cannot be fetched produces no rows, and errors go to the log and `SUMMARY` rather
than the dataset — so a failed document costs nothing.

### Typical uses

- **RAG ingestion.** Clean, chunked text with page numbers, straight into a vector store.
- **Search indexes.** Text that matches what a user actually types, ligatures and all.
- **Analysis.** Pull a set of reports or filings and work on the text.
- **AI agents.** Flat JSON, no key, sensible defaults that work on the first call.

### Notes on data and compliance

- The caller supplies every URL. This Actor does not crawl, follow links, or discover documents.
- Nothing behind a login is accessed, and no bot protection is bypassed.
- Documents over 60 MB are refused rather than silently truncated.

### Limitations — stated up front

- **Scanned PDFs return nothing.** There is no OCR here; a PDF with no text layer has no text to
  extract, and the row count will tell you so.
- Tables lose their column structure. The text is there, the grid is not.
- DOCX, XLSX and PPTX are not supported yet.
- HTML extraction is heuristic: it drops nav, headers, footers, asides, scripts and styles, which
  is right for articles and documentation and less right for pages that are mostly interface.

# Actor input Schema

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

URLs of PDF, HTML or plain-text documents. You supply the links, so this reads only what you point it at.

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

0 returns one row per page or document. Above 0 splits the text into overlapping chunks on paragraph boundaries, each carrying its page number — ready for embedding.

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

How much text to repeat between consecutive chunks so a sentence is not cut in half. Ignored when chunk size is 0.

## `maxPages` (type: `integer`):

0 means the whole document.

## `keyword` (type: `string`):

Keep only pages or chunks containing any of these words.

## Actor input object example

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762"
  ],
  "chunkSize": 0,
  "chunkOverlap": 200,
  "maxPages": 0
}
```

# Actor output Schema

## `text` (type: `string`):

Every page or chunk, as plain text.

## `textCsv` (type: `string`):

The same rows as CSV.

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

What each document resolved to and how many rows it produced.

# 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://arxiv.org/pdf/1706.03762"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainapi/pdf-html-document-text-extractor-for-rag").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://arxiv.org/pdf/1706.03762"] }

# Run the Actor and wait for it to finish
run = client.actor("plainapi/pdf-html-document-text-extractor-for-rag").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://arxiv.org/pdf/1706.03762"
  ]
}' |
apify call plainapi/pdf-html-document-text-extractor-for-rag --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainapi/pdf-html-document-text-extractor-for-rag"
        }
    }
}

```

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/lVXbhp36pNw445PGz/builds/keVe9wVG1T6rOh4Kb/openapi.json
