# Document to Markdown for LLM & RAG (PDF, DOCX, PPTX, HTML) (`divinxxii/doc-to-markdown`) Actor

Convert PDF, Word, PowerPoint and HTML into clean Markdown with optional heading-aware RAG chunks and page numbers. No OCR, no AI costs.

- **URL**: https://apify.com/divinxxii/doc-to-markdown.md
- **Developed by:** [James Brown](https://apify.com/divinxxii) (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 $7.00 / 1,000 document converteds

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 Markdown for RAG (also DOCX, PPTX, HTML)

This PDF to Markdown converter turns **PDF, Word (DOCX), PowerPoint (PPTX) and HTML** into clean, LLM-ready **Markdown**, with optional **heading-aware RAG chunks** (`text`, `heading_path`, `page`) and page numbers. One actor for your whole document pile, fast and cheap: no OCR, no AI model calls, no GPU. A 100-page PDF converts in about 3 seconds using under 100 MB of RAM.

Use it to prepare documents for vector databases, ChatGPT/Claude context windows, search indexes, knowledge bases and document pipelines, or to monitor documents for changes.

### What the PDF to Markdown converter gives you

- **Multi-format**: PDF, DOCX, PPTX and HTML in the same run (format is detected from file content, not the extension).
- **Structure preserved**: headings (`#`..), lists, tables (DOCX, PPTX, HTML), bold/italic (DOCX, HTML), slide notes (PPTX). PDF headings are inferred from font size/weight.
- **RAG chunks**: `[{index, text, heading_path, page}]`. Chunks never cross a heading boundary, so `heading_path` (e.g. `["3. Outlook", "3.1 Risks"]`) is exact. Configurable size and overlap.
- **Page numbers**: `<!-- page: N -->` markers and a `page` on every chunk (PDF pages, PPTX slides).
- **Scanned pages flagged, not OCR'd**: image-only PDF pages and empty slides are listed in `scannedPages` and **not charged**.
- **Clean PDFs**: repeated running headers, footers and page numbers are removed; line-wrap hyphenation is repaired.
- **Monitoring mode**: re-run on a schedule; unchanged documents are skipped and free, new/changed ones are converted (`changeStatus: new | changed | unchanged`).
- **Polite and safe**: honours robots.txt, one request per host per second by default, refuses private/internal addresses (every redirect hop checked), 50 MB size cap. robots.txt is matched per RFC 9309 (wildcards and `$` supported) and fails closed: if robots.txt returns a server error, 401/403 or cannot be reached, the host is skipped.
- Credentials in URLs (`user:pass@host`) are not sent: the actor never makes authenticated requests, so use a URL that needs no login.

### Input

| Field | Default | Meaning |
|---|---|---|
| `urls` | RFC 9111 sample | Public http(s) links to PDF/DOCX/PPTX/HTML |
| `keyValueStoreKeys` | – | Keys of files you uploaded to the run's default key-value store |
| `enableChunking` | `true` | Output RAG chunks |
| `chunkSize` / `chunkOverlap` | `1000` / `100` | Characters (about 4 per token) |
| `includePageNumbers` | `true` | Page markers + chunk `page` |
| `maxPagesPerDocument` | `500` | Only the first N PDF pages are processed and billed |
| `htmlMainContentOnly` | `true` | Drop nav/header/footer for HTML |
| `monitoringMode` | `false` | Skip unchanged documents |
| `maxDocuments` | `100` | Safety cap per run |
| `requestDelaySeconds` | `1` | Pause between requests to one host (minimum 1 s; robots.txt is always honoured, there is no switch to turn it off) |

**No input?** The actor converts the first 5 pages of a public sample PDF (IETF RFC 9111) so you can see real output immediately (about $0.025).

**Uploading files:** put them in the run's default key-value store (Console → Storage → Key-value stores, or the API) and list their keys in `keyValueStoreKeys`.

Example input:

```json
{
  "urls": ["https://www.rfc-editor.org/rfc/rfc9111.pdf", "https://example.com/report.docx"],
  "keyValueStoreKeys": ["board-deck.pptx"],
  "enableChunking": true,
  "chunkSize": 800,
  "includePageNumbers": true
}
```

### Example output: Markdown and RAG chunks (one dataset item per document)

```json
{
  "source": "https://www.rfc-editor.org/rfc/rfc9111.pdf",
  "sourceType": "url",
  "status": "ok",
  "format": "pdf",
  "title": "RFC 9111: HTTP Caching",
  "pageCount": 35,
  "pageCountEstimated": false,
  "billablePages": 5,
  "scannedPages": [],
  "markdown": "<!-- page: 1 -->\n\n# RFC 9111 HTTP Caching\n\n## Abstract\n\nThe Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol ...",
  "chunks": [{"index": 0, "text": "The Hypertext Transfer Protocol (HTTP) ...", "heading_path": ["RFC 9111 HTTP Caching", "Abstract"], "page": 1}],
  "chunkCount": 10,
  "characters": 7921,
  "changeStatus": null,
  "contentSha256": "2663227a...",
  "warnings": ["Only the first 5 of 35 pages were processed (maxPagesPerDocument)."],
  "error": null,
  "processedAt": "2026-10-21T18:41:07+00:00"
}
```

Failed documents are still returned (`status: "failed"`, `error` explains why: robots.txt block, HTTP error, password-protected, unsupported type...) and are never charged.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Document converted | $0.01 | Per successfully converted document |
| Page converted | $0.002 | Per page/slide successfully converted to text |

| Example | Approximate cost |
|---|---|
| One 10-page PDF | $0.035 |
| One 80-page PDF | $0.175 |
| 100 documents of 10 pages | $3.005 |

Not charged: failed or blocked documents, unsupported files, fully scanned documents, scanned/empty pages, and unchanged documents in monitoring mode. Example: one 80-page PDF = $0.005 + $0.01 + $0.16 = $0.175. DOCX files have no verifiable per-page text map: `pageCount` is Word's saved count or an estimate, but **only pages the extracted text accounts for (~500 words each, never more than `pageCount`) are billed** (`billablePages`). Image-only, blank or unsupported DOCX content is flagged in `warnings`, not billed. HTML counts as one page. Output is saved to the dataset **before** charging, so you are never billed for output that was not stored. Set a maximum cost per run in Apify to cap spend; the actor stops cleanly when it is reached.

### How it compares (checked 21 Oct 2026; verify on the Store pages)

- **gochujang/pdf-text-extractor**: PDF only; offers `rag_chunks`, optional Tesseract OCR, table detection, password-protected PDFs, share-link resolution; $0.02/PDF + $0.001/page. **Pick it if** you need OCR, passwords or per-page pricing on PDF-heavy workloads.
- **clearpath/pdf-to-markdown-api**: PDF only; AI layout detection (multi-column, tables, formulas), OCR, image extraction; no chunks; $0.01/PDF + $0.005/page. **Pick it if** you need the highest-fidelity layout/OCR on complex scientific or scanned PDFs.
- **This actor**: **one actor for PDF + DOCX + PPTX + HTML**, **heading-path chunks** with page numbers, **new/changed monitoring**, tiny footprint and speed (no models), cheaper than clearpath per page. It does **not** do OCR, password-protected PDFs or complex PDF table reconstruction.

### Limitations

- **No OCR.** Scanned/image-only pages are flagged (`scannedPages`) and free; their text is not extracted.
- PDF headings, lists and paragraphs are inferred heuristically from fonts and spacing. Multi-column layouts may interleave, and **PDF tables are returned as text, not Markdown tables**. A table of contents can come out as one long paragraph.
- Password-protected/encrypted PDFs are reported as failed. Legacy `.doc`, `.ppt` are unsupported. Images, charts and equations are skipped.
- DOCX/PPTX: headers, footers, text boxes and footnotes in DOCX are not extracted; page numbers do not exist for DOCX/HTML (`page: null`).
- URLs must be publicly reachable without login (no login or paywall bypass). Hosts that disallow automated access in robots.txt are skipped.
- Max 50 MB per file, 2000 pages per PDF. Very short PDF pages (e.g. a title page) are kept and listed in `sparsePages`; a short-text page that also contains an image is treated as likely scanned (text kept, flagged in `scannedPages`, not charged).
- **Restarts and migrations.** The run keeps a small progress record (`RUN_PROGRESS`: source label, delivery/billing stage and charge counts, no document content) in its default key-value store, under standard Apify retention. After a restart or migration, sources already delivered are skipped and nothing is saved or charged twice. If a restart interrupts a charge call whose outcome is unknown, that charge is not repeated, so you are never billed twice for it.

### Legal, data and licences

- **No third-party data source.** The actor only processes files and URLs **you** supply, so there is no source-site ToS to review; you are responsible for having the right to process the documents. Robots.txt is always honoured for URLs (it cannot be switched off). Do not use it to process personal data without a lawful basis (UK/EU GDPR); what is stored: the **dataset** of your run holds the converted Markdown/chunks plus the source URL/key (kept per your Apify plan's dataset retention, deletable by you at any time); downloaded files are written to a temporary file in the run container and deleted right after parsing; uploaded files stay in your own key-value store until you delete them; in monitoring mode a separate named key-value store keeps, per source, a SHA-256 hash, the source name and a timestamp (persists until you delete it). No data is sent to any third party.
- **Dependency licence check (permissive only, no copyleft; verified from installed package metadata of the full resolved tree, 21 Oct 2026). HTTP is done with the Python standard library, so `httpx` and its MPL-2.0 `certifi` dependency are not installed; TLS uses the container's system CA store. Other transitive packages (Apify SDK/crawlee, `impit`, pydantic, cryptography, ...) are MIT, BSD, Apache-2.0, ISC, Unlicense or MIT-0:**

| Package | Licence |
|---|---|
| pypdfium2 (PDFium) | BSD-3-Clause / Apache-2.0 (not PyMuPDF, which is AGPL) |
| python-docx, python-pptx | MIT |
| beautifulsoup4, markdownify | MIT |
| lxml | BSD-3-Clause |
| protego (robots.txt) | BSD-3-Clause |
| apify SDK | Apache-2.0 |

- Sample PDF: IETF RFC 9111 © IETF Trust, redistribution permitted under the IETF Trust Legal Provisions; rfc-editor.org robots.txt allows the path.

### FAQ

**How do I convert a PDF to Markdown for RAG?**
Add the PDF link to `urls` (or upload the file and list its key in `keyValueStoreKeys`). Each document returns a `markdown` field and, with `enableChunking`, a list of chunks with `heading_path` and `page`, ready for a vector database.

**Does it convert DOCX, PPTX and HTML as well?**
Yes. PDF, DOCX, PPTX and HTML can be mixed in one run; the format is detected from the file content.

**Does it do OCR on scanned PDFs?**
No. Scanned pages are listed in `scannedPages` and are not charged.

**Am I charged for failed documents?**
No. Failed, blocked, fully scanned and unchanged documents are free.

### Related Actors

- [Email Validator & List Cleaner](https://apify.com/divinxxii/email-list-cleaner): validate and clean email lists (MX, disposable domains, typos).
- [Phone Number Validator & Formatter](https://apify.com/divinxxii/phone-validator): validate and format phone numbers in bulk, offline.
- [Domain Health Checker](https://apify.com/divinxxii/domain-health-audit): SPF, DMARC, DKIM and SSL expiry checks for any domain.

### Run locally

```bash
pip install -r requirements.txt
python tests/make_fixtures.py ./fixtures            # needs requirements-dev.txt; makes DOCX, PPTX, 100-page PDF, scanned PDF
APIFY_PURGE_ON_START=0 python tests/seed_local_kvs.py ./fixtures   # "upload" them to the local key-value store
## put input in storage/key_value_stores/default/INPUT.json, then:
python -m src            # or: apify run
pytest tests -q
```

### Publishing notes (pay-per-event)

`.actor/pay_per_event.json` documents the three events and prices; **Apify reads pricing from Console, not from this file**, so set it in Console → Monetization → Pay per event (a manual, one-time step that is not verified by this repo):

| Event | Price | Who charges it |
|---|---|---|
| `apify-actor-start` | $0.005 | Apify, automatically, once per run. Runs up to 1 GB are charged once; each extra GB adds another event. `maxMemoryMbytes` is therefore capped at 1024 so a run costs exactly one $0.005 start event. Never charge it in code (the call fails). Do not also create a custom `actor-start` event. |
| `document` | $0.01 | `Actor.charge`, after the result is saved |
| `page` | $0.002 | `Actor.charge`, after the result is saved, for verified text pages only |

**Do not define `apify-default-dataset-item`** (remove it in Console if it is pre-filled): it would bill every dataset record, including failed and unchanged ones, which must stay free. Memory: the 1 GB cap is ample (measured ≈ 95 MB RSS for a 194-page PDF; files are streamed to disk and capped at 50 MB).

# Actor input Schema

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

Public http(s) links to PDF, DOCX, PPTX or HTML documents. robots.txt of each host is honoured.

## `keyValueStoreKeys` (type: `array`):

Keys of document files stored in the run's default key-value store (upload them in the Console under Storage > Key-value stores, or via the API, then list their keys here).

## `enableChunking` (type: `boolean`):

Also output heading-aware chunks: \[{text, heading_path, page}]. Chunks never cross a heading boundary.

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

Target maximum chunk length in characters (about 4 characters per token, so 1000 is roughly 250 tokens).

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

Characters repeated from the end of the previous chunk within the same section. 0 disables overlap.

## `includePageNumbers` (type: `boolean`):

Insert  markers in the Markdown and fill the page field of each chunk. PDF pages and PPTX slides only.

## `maxPagesPerDocument` (type: `integer`):

Only the first N pages of each PDF are processed (and billed). Protects your budget on very large files.

## `htmlMainContentOnly` (type: `boolean`):

For HTML input, drop navigation, headers, footers and sidebars when no <main> or <article> element exists, and use <main>/<article> when present.

## `monitoringMode` (type: `boolean`):

Remember a content hash for every source between runs. Unchanged documents are reported as changeStatus 'unchanged' without Markdown and are not charged. New or changed documents are converted as normal.

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

Safety cap on the number of sources processed in one run.

## `requestDelaySeconds` (type: `integer`):

Pause between requests to one host. Minimum (and default) 1 second; lower values are raised to 1. robots.txt is always honoured.

## Actor input object example

```json
{
  "urls": [
    "https://www.rfc-editor.org/rfc/rfc9111.pdf"
  ],
  "enableChunking": true,
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "includePageNumbers": true,
  "maxPagesPerDocument": 500,
  "htmlMainContentOnly": true,
  "monitoringMode": false,
  "maxDocuments": 100,
  "requestDelaySeconds": 1
}
```

# Actor output Schema

## `results` (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 = {
    "urls": [
        "https://www.rfc-editor.org/rfc/rfc9111.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("divinxxii/doc-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.rfc-editor.org/rfc/rfc9111.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("divinxxii/doc-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.rfc-editor.org/rfc/rfc9111.pdf"
  ]
}' |
apify call divinxxii/doc-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,divinxxii/doc-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/fMKmx1V9gPOwvxAoj/builds/WU86uE3SdXGx6kRmx/openapi.json
