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

Convert PDF, Word, PowerPoint, Excel, HTML and scanned images to clean Markdown or token-sized chunks for RAG, LangChain and LlamaIndex. Headings, tables, images and Tesseract OCR included. Pay per page.

- **URL**: https://apify.com/tidewell/doc-to-markdown.md
- **Developed by:** [Tidewell Software](https://apify.com/tidewell) (community)
- **Categories:** AI, Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

### Document to Markdown for LLM & RAG pipelines

Turn **PDF, Word (DOCX), PowerPoint (PPTX), Excel (XLSX/XLS), HTML, EPUB, CSV and scanned images** into clean, structured **Markdown**, or into **token-sized chunks with page and heading metadata**, ready for embeddings, RAG, LangChain, LlamaIndex, n8n and Make.

- **Real structure.** Headings come from font sizes and weights, so `#`, `##` and `###` follow the document. Lists stay lists, and tables become Markdown tables. Reading order works across columns, and running headers, footers and page numbers are removed.
- **OCR built in.** Tesseract OCR runs only on scanned pages and images, in 25 languages. Pages that already have a text layer are read directly, which is faster and exact.
- **RAG-ready chunks.** Split by heading, or by N tokens with overlap. Every chunk has `page_start`, `page_end`, `heading_path` and a token count.
- **Images.** Embedded images can be saved to the key-value store and linked from the Markdown.
- **Pay per page.** You pay only for pages that produced text. Failed, encrypted and empty files are free.

Docs, examples, MCP setup and an honest comparison with other actors: [plugin.benjaminbritton.co.uk/doc-to-markdown/](https://plugin.benjaminbritton.co.uk/doc-to-markdown/)

#### What can you use it for?

| Use case | How |
|---|---|
| **RAG knowledge base** from PDFs, policies, manuals and contracts | `outputFormat: json`, `chunking: tokens`, then embed `chunks[].text` |
| **LLM-ready context** for agents (Claude, GPT, Gemini) | `outputFormat: markdown`, then pass `markdown` to the model |
| **Scanned archives** (invoices, letters, faxes) to searchable text | Leave OCR on and set `ocrLanguages` |
| **Slide decks and spreadsheets** into a vector store | PPTX gives one page per slide, with speaker notes. XLSX gives one Markdown table per sheet |
| **Convert files your crawler found** | Pass a `datasetId` from Website Content Crawler or any scraper, plus a `datasetUrlField` |
| **n8n / Make / Zapier automations** | Call the Actor with a file URL and read the dataset row |

#### Supported formats

| Format | Engine | Notes |
|---|---|---|
| PDF (digital) | pdfminer.six + pdfplumber (MIT), PDFium (BSD/Apache) | Headings, lists, ruled tables, images, header/footer removal, encrypted PDFs with `pdfPassword` |
| PDF (scanned) | Tesseract 5 OCR (Apache-2.0) | Auto-detected per page. Rotated scans are corrected |
| DOCX | Microsoft MarkItDown (MIT) + mammoth | Headings, lists, tables, bold/italic, images |
| PPTX | MarkItDown + python-pptx | One page per slide, with tables, speaker notes and images |
| XLSX / XLS | openpyxl / xlrd | One Markdown table per sheet. Formulas without cached values are shown as `=FORMULA` |
| HTML | MarkItDown + BeautifulSoup | Keeps `<main>`/`<article>`, drops nav/footer/scripts and makes links absolute |
| PNG, JPEG, TIFF (multi-page), WebP, GIF, BMP | Tesseract OCR | |
| EPUB, CSV, JSON, XML, TXT, MD, IPYNB | MarkItDown | |

Google Drive, Dropbox and GitHub share links are converted to direct downloads automatically.

#### Input example

```json
{
  "fileUrls": [
    "https://arxiv.org/pdf/1706.03762v7",
    "https://example.com/handbook.docx",
    "https://example.com/scanned-invoice.png"
  ],
  "outputFormat": "json",
  "chunking": "tokens",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "ocr": true,
  "ocrLanguages": ["eng"],
  "extractTables": true,
  "extractImages": false
}
```

The main options are listed below. The Input tab documents all of them.

| Field | Default | What it does |
|---|---|---|
| `fileUrls` | | File links |
| `uploadedFiles` | | Upload files in Console |
| `keyValueStoreId`, `keyValueStoreKeys`, `keyValueStorePrefix` | | Convert files from a key-value store |
| `datasetId`, `datasetUrlField` | `url` | Take URLs from a dataset |
| `outputFormat` | `markdown` | `markdown` or `json` (chunks) |
| `chunking` | `none` | `none` (one chunk per page), `heading` or `tokens` |
| `chunkSize`, `chunkOverlap` | 512 / 64 | Size and overlap in cl100k_base tokens |
| `ocr`, `forceOcr`, `ocrLanguages`, `ocrDpi` | on / off / `eng` / 300 | OCR settings |
| `pageRange` | all | Pages to convert, e.g. `1-5,8,10-`. Applies to PDF pages, slides, sheets and TIFF frames |
| `extractTables` | on | Markdown tables from PDF ruling lines |
| `extractImages`, `minImageSize` | off / 64 px | Save images to the key-value store and link them |
| `pageMarkers` | off | Add `<!-- page N -->` comments for citations |
| `pdfPassword` | | Password for encrypted PDFs |
| `maxFileSizeMb`, `documentTimeoutSecs`, `maxConcurrency` | 100 / 300 / 3 | Limits |

#### Output example

One dataset row per document. The full `.md` file (and `chunks.json` in JSON mode) is always saved to the run's key-value store and linked from the row.

```json
{
  "doc_id": "doc-0001",
  "status": "succeeded",
  "source_url": "https://arxiv.org/pdf/1706.03762v7",
  "filename": "1706.03762v7.pdf",
  "mime": "application/pdf",
  "title": "Attention Is All You Need",
  "pages": 15,
  "pages_processed": 15,
  "ocr_pages": 0,
  "tables_count": 0,
  "images": [
    { "key": "doc-0001-1706.03762v7-img-001.png", "url": "https://api.apify.com/v2/key-value-stores/.../records/doc-0001-1706.03762v7-img-001.png", "page": 3, "width": 1520, "height": 2239, "mime": "image/png" }
  ],
  "word_count": 6025,
  "token_count": 9474,
  "markdown_url": "https://api.apify.com/v2/key-value-stores/.../records/doc-0001-1706.03762v7.md",
  "chunk_count": 24,
  "chunks": [
    {
      "chunk_id": "doc-0001#2",
      "chunk_index": 2,
      "text": "## 1 Introduction\n\nRecurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks ...",
      "token_count": 447,
      "page_start": 2,
      "page_end": 2,
      "heading": "1 Introduction",
      "heading_path": ["Attention Is All You Need", "1 Introduction"],
      "source_url": "https://arxiv.org/pdf/1706.03762v7",
      "filename": "1706.03762v7.pdf"
    }
  ],
  "processing_ms": 5937,
  "errors": [],
  "warnings": []
}
```

Failed documents get `status: "failed"` with an error code, such as `encrypted_pdf`, `corrupt_file`, `download_failed`, `file_too_large`, `unsupported_format`, `no_text` or `timeout`. They are never charged.

The dataset has four views: **Overview**, **Markdown**, **Chunks** (one row per chunk) and **Status & errors**.

#### Use with LangChain

```python
from apify_client import ApifyClient
from langchain_core.documents import Document

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("tidewell/doc-to-markdown").call(run_input={
    "fileUrls": ["https://arxiv.org/pdf/1706.03762v7"],
    "outputFormat": "json", "chunking": "tokens", "chunkSize": 512, "chunkOverlap": 64,
})

docs = [
    Document(page_content=c["text"],
             metadata={"source": row["source_url"], "page": c["page_start"], "heading": c["heading"]})
    for row in client.dataset(run["defaultDatasetId"]).iterate_items() if row["status"] == "succeeded"
    for c in row["chunks"]
]
## -> vectorstore.add_documents(docs)
```

If you prefer to chunk with your own splitter (for example `MarkdownHeaderTextSplitter`), use `outputFormat: "markdown"` and read `row["markdown"]`.

#### Use with LlamaIndex

```python
from apify_client import ApifyClient
from llama_index.core import VectorStoreIndex
from llama_index.core.schema import TextNode

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("tidewell/doc-to-markdown").call(run_input={
    "fileUrls": ["https://example.com/handbook.pdf"], "outputFormat": "json", "chunking": "heading",
})
nodes = [
    TextNode(text=c["text"], id_=c["chunk_id"],
             metadata={"source": row["source_url"], "page": c["page_start"], "section": " > ".join(c["heading_path"])})
    for row in client.dataset(run["defaultDatasetId"]).iterate_items() if row["status"] == "succeeded"
    for c in row["chunks"]
]
index = VectorStoreIndex(nodes)
```

#### Use from the API, n8n or Make

Run it synchronously and get the rows back in one HTTP call:

```bash
curl -X POST "https://api.apify.com/v2/acts/tidewell~doc-to-markdown/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"fileUrls": ["https://example.com/report.pdf"], "outputFormat": "markdown"}'
```

In n8n or Make, use the Apify node with the action "Run Actor and get dataset items".

#### Use from Claude / ChatGPT (MCP)

Run this actor from an AI assistant through Apify's hosted MCP server. This server URL loads only this actor:

```
https://mcp.apify.com/?tools=tidewell/doc-to-markdown
```

- **Claude (claude.ai and Claude Desktop):** Settings → Connectors → Add custom connector. Name it `Apify`, paste the URL, then sign in to Apify.
- **Claude Code:** `claude mcp add apify "https://mcp.apify.com/?tools=tidewell/doc-to-markdown" -t http`
- **ChatGPT:** add a custom MCP server named `Apify` with the URL above and OAuth. In the desktop app: Settings → MCP servers → Add server → Streamable HTTP.
- **Cursor, VS Code and other MCP clients:** `{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?tools=tidewell/doc-to-markdown"}}}`. Clients without OAuth can add the header `Authorization: Bearer <APIFY_TOKEN>`.

Then ask in plain language, for example: *"Use tidewell/doc-to-markdown to convert https://arxiv.org/pdf/1706.03762v7 to Markdown and summarise section 3."* The actor appears as the tool `tidewell--doc-to-markdown`. Runs are billed to your Apify account at the prices below.

Full setup guide, examples and comparisons: [plugin.benjaminbritton.co.uk/doc-to-markdown/](https://plugin.benjaminbritton.co.uk/doc-to-markdown/)

#### Pricing

Pay per event. You pay only for output.

| Event | Price | When |
|---|---|---|
| Document converted | **$0.002** | Once per document that converts successfully |
| Page converted | **$0.0003** | Per page that produced text (PDF page, slide, image or TIFF frame) |
| OCR page | **+$0.0027** | Extra per page read with OCR, so an OCR page costs $0.003 in total |

DOCX, HTML, XLSX, EPUB and text files have no fixed pages, so they are billed at **one page per 500 words** (minimum 1).

| Example | Cost |
|---|---|
| 10-page digital PDF | $0.005 |
| 100-page digital PDF | $0.032 |
| 10-page scanned PDF (OCR) | $0.032 |
| 1,000 one-page Word memos | $2.30 |
| 1,000 photographed receipts (OCR) | $5.00 |

Failed downloads, corrupt, encrypted, unsupported and empty files cost nothing, and neither do pages without text. The Actor respects your **maximum cost per run**. It caps pages before converting, so it never does work it cannot bill.

#### FAQ

**Is the text from digital PDFs OCR'd?** No. Pages with a text layer are read directly, which is exact and about 10x faster. OCR runs only on pages without usable text, or on every page if you set `forceOcr`.

**How good is table extraction?** Tables with ruling lines (Word, Excel and most business PDFs) come out as Markdown tables. Tables drawn with whitespace alone, such as many academic papers, come out as aligned text lines. Word, PowerPoint, Excel and HTML tables are always converted.

**Does it handle two-column PDFs?** Yes. Reading order follows pdfminer's layout analysis, so columns are read one after the other instead of line by line across the page.

**Which OCR languages are available?** eng, deu, fra, spa, ita, por, nld, pol, ces, swe, dan, nor, fin, tur, rus, ukr, ell, ara, heb, hin, chi_sim, chi_tra, jpn, kor, vie. You can combine them, e.g. `["eng", "deu"]`.

**What about password-protected PDFs?** Set `pdfPassword`. Without it the document fails with `encrypted_pdf` and is not charged.

**How big can files be?** 100 MB by default, and up to 1,000 MB with `maxFileSizeMb`. Each document runs in its own process with a timeout, so one broken file cannot stop the run.

**Are my documents stored?** Only in your own Apify run storage, as the dataset and key-value store, under your retention settings. Nothing is sent to third-party AI services. Conversion runs entirely inside the Actor.

**Is it a web crawler?** No. It converts files you point it at. To crawl a site first, run a crawler and pass its dataset here with `datasetId`.

**Which tokenizer is used for chunk sizes?** `cl100k_base` (OpenAI), which gives a close estimate for most embedding models.

#### Keywords

pdf to markdown, docx to markdown, pptx to markdown, xlsx to markdown, html to markdown, document parser, document to markdown, pdf text extractor, pdf parser api, ocr, ocr api, scanned pdf to text, image to text, tesseract, rag, rag chunking, llm ready, llm document loader, langchain document loader, llamaindex, markitdown, unstructured alternative, docling alternative, chunk pdf for embeddings, pdf to json

# Actor input Schema

## `fileUrls` (type: `array`):

Direct links to PDF, DOCX, PPTX, XLSX/XLS, HTML, EPUB, CSV, JSON, XML, TXT/MD files or PNG/JPEG/TIFF/WebP/GIF/BMP images. Google Drive, Dropbox and GitHub share links are converted to direct downloads automatically.

## `uploadedFiles` (type: `array`):

Upload files from your computer. They are stored in a key-value store and processed like file URLs.

## `keyValueStoreId` (type: `string`):

Convert files stored in an Apify key-value store (for example the output of another Actor). Use "default" for this run's own store.

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

Keys to convert. Leave empty to convert every record in the store (optionally filtered by prefix).

## `keyValueStorePrefix` (type: `string`):

When no keys are listed, only records whose key starts with this prefix are converted.

## `datasetId` (type: `string`):

Take file URLs from the items of an Apify dataset, e.g. links collected by a crawler.

## `datasetUrlField` (type: `string`):

Item field that holds the file URL (a string or a list of strings).

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

markdown: each dataset row holds the full Markdown. json: each row holds chunks\[] with page and heading metadata (the full Markdown is still saved to the key-value store).

## `chunking` (type: `string`):

none: one chunk per page (JSON output). heading: one chunk per section, long sections split by tokens. tokens: fixed-size token windows with overlap that respect paragraph and table boundaries.

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

Maximum tokens per chunk (cl100k_base tokenizer, as used by OpenAI embedding models).

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

Tokens repeated from the end of the previous chunk. Capped at half the chunk size.

## `pageMarkers` (type: `boolean`):

Add  comments between pages in the Markdown (PDF, slides, multi-page images). Useful for citations.

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

Run Tesseract OCR on images and on PDF pages without a usable text layer. Pages with a text layer are read directly (faster and exact).

## `forceOcr` (type: `boolean`):

OCR every page even if it has a text layer. Use for PDFs whose embedded text is garbled.

## `ocrLanguages` (type: `array`):

Tesseract language codes. Installed: eng, deu, fra, spa, ita, por, nld, pol, ces, swe, dan, nor, fin, tur, rus, ukr, ell, ara, heb, hin, chi_sim, chi_tra, jpn, kor, vie. Combine several for mixed documents (slower).

## `ocrDpi` (type: `integer`):

Resolution at which scanned PDF pages are rendered for OCR. 300 is a good balance; use 400 for small print.

## `pageRange` (type: `string`):

Pages to convert, e.g. "1-5,8,10-". Applies to PDF pages, slides, spreadsheet sheets and multi-page TIFF frames. Empty = all.

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

Safety cap on pages converted per document.

## `extractTables` (type: `boolean`):

Detect ruled tables in PDFs and output them as Markdown tables. Word, PowerPoint, Excel and HTML tables are always converted to Markdown tables.

## `extractImages` (type: `boolean`):

Save embedded images to the key-value store and link them from the Markdown (![...](url)).

## `minImageSize` (type: `integer`):

Skip images narrower or shorter than this (icons, bullets, spacer images).

## `maxImagesPerDocument` (type: `integer`):

Stop extracting images after this many per document.

## `removeHeadersFooters` (type: `boolean`):

Drop text repeated at the top or bottom of most PDF pages.

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

Drop navigation, headers, footers and sidebars from HTML pages and keep <main>/<article> when present.

## `pdfPassword` (type: `string`):

Password for encrypted PDFs (used for every PDF in the run).

## `httpHeaders` (type: `object`):

Headers sent with every download, e.g. {"Authorization": "Bearer ..."} for private file servers.

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

Files larger than this are skipped (not charged).

## `documentTimeoutSecs` (type: `integer`):

Conversion of one document is stopped after this time and reported as failed (not charged).

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

Maximum time to download one file.

## `maxConcurrency` (type: `integer`):

Documents converted in parallel. Each extra 4 GB of memory gives the run one more CPU core.

## Actor input object example

```json
{
  "fileUrls": [
    "https://arxiv.org/pdf/1706.03762v7"
  ],
  "datasetUrlField": "url",
  "outputFormat": "markdown",
  "chunking": "none",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "pageMarkers": false,
  "ocr": true,
  "forceOcr": false,
  "ocrLanguages": [
    "eng"
  ],
  "ocrDpi": 300,
  "maxPagesPerDocument": 1000,
  "extractTables": true,
  "extractImages": false,
  "minImageSize": 64,
  "maxImagesPerDocument": 200,
  "removeHeadersFooters": true,
  "htmlMainContentOnly": true,
  "maxFileSizeMb": 100,
  "documentTimeoutSecs": 300,
  "downloadTimeoutSecs": 120,
  "maxConcurrency": 3
}
```

# Actor output Schema

## `documents` (type: `string`):

One row per document with Markdown or chunks and metadata.

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

Full .md files and extracted images in the key-value store.

# 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 = {
    "fileUrls": [
        "https://arxiv.org/pdf/1706.03762v7"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidewell/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 = { "fileUrls": ["https://arxiv.org/pdf/1706.03762v7"] }

# Run the Actor and wait for it to finish
run = client.actor("tidewell/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 '{
  "fileUrls": [
    "https://arxiv.org/pdf/1706.03762v7"
  ]
}' |
apify call tidewell/doc-to-markdown --silent --output-dataset

```

## MCP server setup

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