# PDF to Text & Markdown Extractor (offline, RAG chunks) (`wrentools/pdf-text-markdown-extractor`) Actor

Extracts plain text, Markdown with headings and lists, per-page text, metadata and RAG-ready chunks from PDFs you link or upload as base64. Runs offline with pdfminer.six: no AI, no third-party API. Respects robots.txt and PDF permissions. Flags scanned pages (no OCR).

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

## Pricing

from $2.00 / 1,000 pdf 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 to Text & Markdown Extractor (offline, RAG chunks)

Turn PDFs into clean text and Markdown you can feed to a search index, an LLM or a spreadsheet. Give it links to PDF
files, or send files inline as base64, and get back for each one: plain text, Markdown with headings and lists
inferred from the layout, optional per-page text, optional RAG-ready chunks with page ranges, and the document's
metadata.

Everything runs inside the Actor with [pdfminer.six](https://github.com/pdfminer/pdfminer.six) (MIT licence). Your
files are not sent to any AI model or third-party API, and the result for the same file is the same every time.

**Code example**, in Python and Node:
[PDF to Markdown and RAG chunks](https://github.com/WrenTools/apify-actor-examples/tree/main/pdf-to-markdown-and-rag-chunks).

### What you get

- **Plain text**, page by page, with paragraph breaks kept.
- **Markdown**: the most common font size is the body text; short lines set clearly larger become `#`, `##` and `###`
  headings, bullet lines become `- ` items, and words hyphenated across a line break are rejoined.
- **Per-page text** (`includePages`), for citing page numbers.
- **RAG chunks** (`chunkSize`): Markdown split at paragraph boundaries into pieces of at most that many characters,
  each with `pageStart` and `pageEnd`, with an optional overlap.
- **Metadata**: title, author, subject, keywords, creator, producer, creation and modification dates.
- **Scanned-page detection**: pages with no text layer are listed in `pagesWithoutText`, and `likelyScanned` is set
  when half or more of the pages have none. This Actor does not run OCR: send those pages to
  [Image & Scanned PDF OCR](https://apify.com/wrentools/scanned-pdf-image-ocr).

Failed files are reported in the dataset with a reason and are **not charged**: `fetch-failed`, `file-too-large`,
`not-a-pdf`, `unreadable-pdf`, `robots-disallowed`, `password-required`, `extraction-not-permitted`,
`invalid-base64`.

### Respectful by design

- **Only the files you name are downloaded**, one request each. The site's robots.txt is read first and obeyed, and
  downloads from one site are spaced (500 ms by default). No proxy, no browser, no login.
- **PDF permissions are honoured.** A PDF that needs a password is refused (the Actor takes no password), and so is a
  PDF whose owner has forbidden copying or extracting its text.
- Only public internet addresses are fetched: a URL or redirect into a private, loopback or link-local network is
  refused.

Process only documents you have the right to use.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `pdfUrls` | array | - | Links to PDF files. |
| `pdfBase64` | array | - | Inline files: base64 strings, or `{"name": "invoice.pdf", "data": "<base64>"}`. A `data:` URL prefix is fine. |
| `outputFormat` | string | `both` | `both`, `markdown` or `text`. |
| `includePages` | boolean | `false` | Adds `pages`: `[{"page": 1, "text": "..."}]`. |
| `chunkSize` | integer | `0` | Characters per RAG chunk; 0 is off, otherwise at least 200. |
| `chunkOverlap` | integer | `200` | At most half the chunk size. |
| `maxPagesPerPdf` | integer | `500` | Longer files are marked `truncated`. |
| `maxFileSizeMb` | integer | `50` | Up to 200. |
| `requestDelayMs` | integer | `500` | Pause between downloads from one site. |

### Output

One dataset item per PDF, in input order:

```json
{"source": "https://example.com/reports/q3.pdf", "url": "https://example.com/reports/q3.pdf",
 "finalUrl": "https://example.com/reports/q3.pdf", "fileName": "q3.pdf", "status": "ok", "error": null,
 "pageCount": 3, "pagesExtracted": 3, "truncated": false,
 "metadata": {"title": "Quarterly Field Report", "author": "Field Team", "creationDate": "2026-10-01T09:00:00"},
 "pagesWithoutText": [3], "likelyScanned": false, "charCount": 512, "wordCount": 88,
 "text": "Quarterly Field Report\n\n1. Summary\n\nThe survey covered ...",
 "markdown": "# Quarterly Field Report\n\n## 1. Summary\n\nThe survey covered ...",
 "chunks": [{"index": 0, "text": "# Quarterly Field Report ...", "pageStart": 1, "pageEnd": 1, "charCount": 287}]}
```

A failed file:

```json
{"source": "https://example.com/locked.pdf", "status": "error",
 "error": {"code": "extraction-not-permitted", "message": "the PDF's owner forbids copying or extracting its text, so it is not extracted"}}
```

A result too large for one dataset item (over 8 MB) keeps its counts and metadata in the dataset; the text, Markdown,
pages and chunks are stored in the run's key-value store, at `fullOutputKey` / `fullOutputUrl`.

Key-value store record `SUMMARY`: PDFs requested, extracted and failed, pages extracted, and why the run stopped early
if it did.

### Pricing

Pay per event, no start fee, and nothing for files that fail:

| Event | Price | Per 1,000 |
|---|---|---|
| `pdf-processed` (one PDF extracted) | $0.002 | $2.00 |
| `page-extracted` (each page of it) | $0.0001 | $0.10 |

A 10-page PDF costs $0.003; a 100-page PDF costs $0.012. When you set a maximum charge for a run, the Actor stops
before a file it cannot pay for, and extracts only the pages the remainder covers (the item is marked `truncated`).

### Limits

- No OCR: scanned pages come back empty and are listed in `pagesWithoutText`.
- Tables come out as text lines; their cell structure is not rebuilt. Multi-column layouts follow pdfminer's reading
  order, which is usually but not always right.
- Markdown headings are inferred from font size, so a document that sets headings in body-size bold reads as text.

# Actor input Schema

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

Links to PDF files you are allowed to process. Each file is downloaded once; the site's robots.txt is respected.

## `pdfBase64` (type: `array`):

PDF files sent inline, for files that are not online: a list of base64 strings, or of objects {"name": "invoice.pdf", "data": "<base64>"}. A data: URL prefix is accepted.

## `outputFormat` (type: `string`):

Plain text, Markdown (headings and lists inferred from the layout), or both.

## `includePages` (type: `boolean`):

Add a 'pages' list with each page's text and number.

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

When above 0, also split the Markdown into chunks of at most this many characters at paragraph boundaries, each with its page range. 0 turns chunking off; the minimum is 200.

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

Characters of the previous chunk repeated at the start of the next (at most half the chunk size).

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

Extract at most this many pages from each PDF; longer files are marked truncated. Pages are charged as extracted.

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

Larger downloads are stopped and reported as file-too-large, without charge.

## `requestDelayMs` (type: `integer`):

Minimum pause between two downloads from the same site.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/field-report.pdf"
  ],
  "outputFormat": "both",
  "includePages": false,
  "chunkSize": 0,
  "chunkOverlap": 200,
  "maxPagesPerPdf": 500,
  "maxFileSizeMb": 50,
  "requestDelayMs": 500
}
```

# Actor output Schema

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

No description

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

No description

# 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://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/field-report.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("wrentools/pdf-text-markdown-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 = { "pdfUrls": ["https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/field-report.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("wrentools/pdf-text-markdown-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 '{
  "pdfUrls": [
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/field-report.pdf"
  ]
}' |
apify call wrentools/pdf-text-markdown-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,wrentools/pdf-text-markdown-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/kXcixadtQUfXUCJjW/builds/huQbx7tQn7jpjlpwA/openapi.json
