# PDF & DOCX to Markdown Converter for AI Agents (`owlabee/pdf-to-markdown`) Actor

Convert PDF, DOCX, HTML and text documents from URLs into clean Markdown with headings, lists, tables, links and metadata, plus optional heading-aware RAG chunks and OCR for scanned pages. No browser, no external API. Pay per page converted ($0.001).

- **URL**: https://apify.com/owlabee/pdf-to-markdown.md
- **Developed by:** [Owlabee](https://apify.com/owlabee) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page converteds

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 & DOCX to Markdown Converter for AI Agents

### What it does

Point this Actor at document URLs and get back clean, LLM-ready **Markdown**: headings with the right levels, bullet and numbered lists, tables as GFM tables, links, page markers and document metadata (title, author, producer, creation date, page count). It reads **PDF** (text layout reconstructed from the PDF's own text layer — no browser, no external API), **DOCX**, **HTML**, **Markdown** and **plain text**, so agents and RAG pipelines can send it anything they find and always get the same shape back.

Optional extras: **heading-aware chunking** (`recordType: chunk` records with page range, heading path and a token estimate, ready to embed), **OCR** for scanned PDF pages (bundled English tesseract model, runs inside the Actor, no data leaves Apify), and writing every document's Markdown as a `.md` file into the run's key-value store.

One dataset record per document; results stream in as each file finishes and are available as JSON, CSV, Excel, via the API, webhooks or the Apify MCP server.

### How to use

1. Enter **Document URLs** — public `https://` links to PDF/DOCX/HTML/text files, `kvs://<storeId>/<key>` references to files you uploaded to an Apify key-value store (`kvs://default/<key>` for the run's own store), or `data:` URLs with inline content.
2. Optionally turn on **Create RAG chunks** (set chunk size and overlap), **OCR scanned pages**, or **Save Markdown files to the key-value store**.
3. Click **Start**. Each document appears in the dataset as soon as it is converted; open the `markdown` field or download the `.md` files.

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `urls` | string\[] | yes | Documents to convert: `http(s)://`, `kvs://<storeId>/<key>` or `data:` URLs, one per entry |
| `chunk` | boolean | no | Also emit heading-aware chunk records (default `false`) |
| `chunkTokens` | integer | no | Target chunk size in estimated tokens (default 800, 100–8000) |
| `chunkOverlapTokens` | integer | no | Overlap repeated from the previous chunk (default 80, capped at half the chunk size) |
| `ocr` | boolean | no | Run OCR on PDF pages without extractable text (default `false`) |
| `saveToKeyValueStore` | boolean | no | Write `<slug>.md` into the default key-value store and record `kvsKey` (default `false`) |
| `pageBreaks` | boolean | no | Keep `<!-- page N -->` markers between PDF pages (default `true`) |
| `maxFileSizeMb` | integer | no | Reject larger files with a `TOO_LARGE` error item (default 25, max 50) |
| `downloadTimeoutSecs` | integer | no | Per-file download timeout (default 20, 5–120) |
| `webhookUrl` | string | no | POST target for the run summary |

### Output example

Document record (`recordType: "document"`), one per input URL:

```json
{
  "recordType": "document",
  "url": "https://arxiv.org/pdf/1706.03762v7",
  "filename": "1706.03762v7.pdf",
  "mimeType": "application/pdf",
  "bytes": 2215244,
  "pages": 15,
  "pagesProcessed": 15,
  "title": null,
  "author": null,
  "producer": "pdfTeX-1.40.25",
  "creationDate": "2024-04-10T21:11:43.000Z",
  "markdown": "<!-- page 1 -->\n\n# Attention Is All You Need\n\n| Ashish Vaswani∗ | Noam Shazeer∗ | ... |\n\n## Abstract\n\nThe dominant sequence transduction models are based on complex recurrent or convolutional neural networks ...\n\n<!-- page 2 -->\n\n## 1 Introduction\n\nRecurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks ...",
  "characters": 39412,
  "words": 6004,
  "headings": [
    { "level": 1, "text": "Attention Is All You Need" },
    { "level": 2, "text": "Abstract" },
    { "level": 2, "text": "1 Introduction" },
    { "level": 3, "text": "3.1 Encoder and Decoder Stacks" }
  ],
  "tables": 12,
  "links": [{ "url": "https://github.com/tensorflow/tensor2tensor", "page": 10, "text": "https://github.com/" }],
  "needsOcrPages": [],
  "ocrPages": [],
  "warnings": ["283 rotated text item(s) skipped (watermarks or sideways labels)"],
  "kvsKey": null,
  "processingMs": 2766,
  "fetchedAt": "2026-10-16T13:20:00.000Z"
}
```

Chunk record (`recordType: "chunk"`), only when `chunk` is on:

```json
{
  "recordType": "chunk",
  "url": "https://arxiv.org/pdf/1706.03762v7",
  "filename": "1706.03762v7.pdf",
  "chunkIndex": 4,
  "chunkCount": 17,
  "startPage": 3,
  "endPage": 4,
  "headingPath": ["Attention Is All You Need", "3 Model Architecture", "3.2 Attention"],
  "markdown": "### 3.2 Attention\n\nAn attention function can be described as mapping a query and a set of key-value pairs to an output ...",
  "tokensEstimate": 572,
  "characters": 2287,
  "fetchedAt": "2026-10-16T13:20:00.000Z"
}
```

Error items look like `{ "recordType": "document", "url": "...", "error": "NOT_FOUND", "errorMessage": "HTTP 404 for ..." }` with codes `INVALID_URL`, `DOWNLOAD_FAILED`, `TIMEOUT`, `TOO_LARGE`, `NOT_FOUND`, `UNSUPPORTED_TYPE`, `CORRUPT_FILE`, `PASSWORD_PROTECTED` or `PROCESSING_FAILED`. They are charged as one page, because the work to discover the problem was done.

### Pricing

Pay per event. You only pay for what the Actor produces; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Notes |
|---|---|---|
| Page converted | $0.001 | Per page of every document record (PDF pages; DOCX declared page count; HTML/text ≈ 1 page per 3,000 characters). Error items count as 1 page |
| OCR page | $0.005 | Per scanned page recognised, only with `ocr: true`, on top of the page price |
| Chunk created | $0.0002 | Per chunk record, only with `chunk: true` |
| Actor start | $0.00005 | Apify platform event |

**Examples:** a 30-page PDF ≈ **$0.03**; 1,000 pages ≈ **$1.00**; chunking those 1,000 pages into ~1,200 chunks adds ≈ $0.24; OCR on a 100-page scan adds $0.50.

### Limits and fair use

- Files up to 25 MB by default (`maxFileSizeMb`, max 50 MB); 20 s download timeout by default; 10 minutes of processing per file, after which the pages converted so far are returned with a `PROCESSING_TIMEOUT` warning.
- Two documents are processed concurrently; a text-based PDF converts at roughly 50–200 pages per second, OCR at 2–5 seconds per page.
- Memory is set to 512–2048 MB (higher than our usual 256–1024 MB default) because pdf.js keeps page content and fonts in memory and OCR renders pages to bitmaps; 50 MB scans with `ocr: true` should run with 2048 MB.
- Layout reconstruction is heuristic: headings come from font-size and bold clusters, tables from column alignment, two-column pages are read column by column, running headers/footers and page numbers are stripped. Complex forms, rotated text, equations and multi-line table cells are best effort. Images are not extracted.
- OCR is English only and ignores the `needsOcrPages` list unless `ocr` is on, so scanned documents cost nothing extra until you ask.
- Only publicly reachable URLs and your own key-value stores are accessed. No proxies, no login, no third-party APIs. The Actor contacts only the URLs you give it and the optional webhook.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [Website Change Monitor — Sitemap & Page Content Diff](https://apify.com/owlabee/website-change-monitor) — find new or changed pages on a site, then convert the documents they link to.
- [Podcast RSS Feed Scraper — Episodes & Transcripts](https://apify.com/owlabee/podcast-rss-scraper) — episode metadata and transcript text from podcast feeds, ready for the same RAG pipeline.
- [US Government Grants & Contract Opportunities Feed](https://apify.com/owlabee/gov-opportunity-feed) — grant and contract opportunities whose attached PDFs this Actor can turn into Markdown.

### FAQ

**Does this need proxies, a browser or an API key?** No. It downloads files with plain HTTPS and converts them in-process with pdf.js, mammoth, turndown and tesseract.js. Nothing is sent to an external conversion service.

**How is cost controlled?** Every page is charged as it is converted and every chunk as it is saved; the Actor stops cleanly when the run's maximum total charge is reached. `maxFileSizeMb` and the 10-minute per-file cap bound the work a single URL can cause. Scanned pages are only OCR'd (and billed at $0.005) when `ocr` is on.

**What happens with scanned PDFs?** Pages with no extractable text are listed in `needsOcrPages` and marked `<!-- page N: no extractable text -->` in the Markdown. Turn on `ocr` to have them recognised; the result is marked `<!-- page N: ocr -->` and the pages appear in `ocrPages`.

**Can an AI agent use it?** Yes — it is pay-per-event, uses limited permissions and returns one predictable record per document, so it is discoverable and callable through the Apify MCP server. Prompts like "convert this PDF to Markdown and give me its headings" or "chunk these three reports into 800-token pieces for my vector store" map directly to the input.

**How do chunks reference pages?** Each chunk carries `startPage`/`endPage` (from the `<!-- page N -->` markers) and `headingPath`, the heading hierarchy the chunk's content lives under, so citations and filters work without re-parsing the document.

**Why is a DOCX or HTML file charged as N pages?** DOCX files declare their last-saved page count (`docProps/app.xml`); when that is missing, and for HTML/Markdown/text, pages are estimated at one per ~3,000 characters (`pagesEstimated: true`).

**What if the URL is not a document?** You get an error item (`UNSUPPORTED_TYPE`, `NOT_FOUND`, ...) instead of a failed run, charged as a single page.

### Legal

Only convert documents you are allowed to access and process. Use the output in compliance with the source's terms and applicable law. This Actor is not a consumer reporting agency and its output must not be used for employment, credit, insurance, or tenant screening decisions.

# Actor input Schema

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

Documents to convert, one per line: public http(s) URLs of PDF, DOCX, HTML, Markdown or plain-text files, Apify key-value-store references in the form kvs://<storeId>/<key> (use kvs://default/<key> for this run's own store), or data: URLs with inline base64 content. Each entry becomes one document record. The prefilled PDFs are small public files used by Apify's daily automated test.

## `chunk` (type: `boolean`):

Also emit one chunk record per heading-aware chunk of the Markdown (recordType: chunk) with chunkIndex, startPage, endPage, headingPath and tokensEstimate — ready for embedding pipelines. Each chunk is charged separately ($0.0002).

## `chunkTokens` (type: `integer`):

Target size of each chunk in estimated tokens (~4 characters per token). Blocks such as tables and paragraphs are kept whole where possible; headings start new chunks once a chunk is half full.

## `chunkOverlapTokens` (type: `integer`):

How much of the previous chunk's tail is repeated at the start of the next chunk, in estimated tokens. Capped at half the chunk size.

## `ocr` (type: `boolean`):

When a PDF page has no extractable text (scanned or image-only), render it and run English OCR (tesseract, bundled, no external API). Pages that would need OCR are always listed in needsOcrPages; this toggle decides whether they are actually recognised. OCR'd pages cost $0.005 each on top of the page price and take 2–5 s per page.

## `saveToKeyValueStore` (type: `boolean`):

Also write each document's full Markdown as <slug>.md into the run's default key-value store and record the key as kvsKey. Useful for documents whose Markdown is too large to read comfortably from the dataset.

## `pageBreaks` (type: `boolean`):

Insert  comments between PDF pages so chunks and citations can reference page numbers. Turn off for Markdown without any HTML comments.

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

Files larger than this are rejected with a TOO_LARGE error item instead of being downloaded. Raise it up to 50 MB for large scans; bigger files need more memory.

## `downloadTimeoutSecs` (type: `integer`):

Maximum time to download one file before it is recorded as a TIMEOUT error item.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a JSON summary of the run (documents, errors, pages, chunks, charges) to this URL when it finishes. Failures are logged, never fatal.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://arxiv.org/pdf/1706.03762v7"
  ],
  "chunk": false,
  "chunkTokens": 800,
  "chunkOverlapTokens": 80,
  "ocr": false,
  "saveToKeyValueStore": false,
  "pageBreaks": true,
  "maxFileSizeMb": 25,
  "downloadTimeoutSecs": 20
}
```

# Actor output Schema

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

One item per converted document, plus one item per chunk when chunking is enabled, in the default dataset (recordType, filename, url, pages, title, words, tables, markdown, chunkIndex; error items carry error and errorMessage). Fields are described in dataset_schema.json.

## `files` (type: `string`):

The converted Markdown documents, one .md record per source file (named after the source filename or title), in the run's default key-value store.

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

SUMMARY record in the run's default key-value store: documents and pages converted, failures and charges, written when the run finishes.

# 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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
        "https://arxiv.org/pdf/1706.03762v7"
    ],
    "chunk": false,
    "ocr": false,
    "saveToKeyValueStore": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/pdf-to-markdown").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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
        "https://arxiv.org/pdf/1706.03762v7",
    ],
    "chunk": False,
    "ocr": False,
    "saveToKeyValueStore": False,
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/pdf-to-markdown").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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://arxiv.org/pdf/1706.03762v7"
  ],
  "chunk": false,
  "ocr": false,
  "saveToKeyValueStore": false
}' |
apify call owlabee/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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

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/aA2tdx64X2B630L9R/builds/WbdDioPsDHWZqs9Wu/openapi.json
