# PDF & Document to Markdown for LLMs (OCR, Tables, RAG Chunks) (`lan460308587/document-to-markdown`) Actor

Convert PDFs, Word, PowerPoint, Excel, HTML and scanned images into clean, LLM-ready Markdown. Keeps headings, tables and two-column reading order, OCRs scanned pages, and outputs heading-aware chunks for RAG. Pay only for documents that convert.

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

## Pricing

from $2.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.

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 & Document to Markdown for LLMs

Turn PDFs, Word, PowerPoint, Excel, HTML and scanned images into **clean Markdown that LLMs and RAG pipelines can use**. Headings, tables and lists are kept, multi-column layouts (papers, government documents, newsletters) are read in the right order, and scanned pages are OCR'd automatically. You can also get **heading-aware chunks** ready for embeddings.

You pay per document and per page. **Documents that fail or contain no text are free**, and OCR is billed only for pages where it finds text.

### What makes the output clean

| Problem with naive PDF text extraction | What this Actor does |
|---|---|
| Multi-column pages come out as interleaved half-lines | Detects 2–4 columns and reads each column in order, keeping full-width titles, mastheads and headings in their place |
| Tables turn into a soup of numbers | Ruled tables become Markdown tables with a header row, one row per record even when the table is only ruled every few rows |
| Every page repeats "Journal of X · Page 12" | Running headers, footers and page numbers are removed |
| Headings are lost, so chunks have no context | Font sizes and bold lines become `#`, `##`, `###` headings |
| Scanned PDFs return nothing | Pages without a text layer (or with garbled fonts) are OCR'd with Tesseract |
| Words split across lines ("exam- ple") | Line-end hyphenation is repaired; compounds like "state-of-the-art" are kept |
| Chunks cut sections in half | Chunks start at section boundaries and carry their section path and page numbers |

### Supported formats

- **PDF**: text PDFs, scanned PDFs (OCR), password-protected PDFs (with `pdfPassword`)
- **Microsoft Office**: Word `.docx`, PowerPoint `.pptx`, Excel `.xlsx` and `.xls`
- **Web and data**: HTML, CSV, JSON, Jupyter notebooks, Markdown, plain text, EPUB
- **Images** (OCR): PNG, JPG, TIFF (multi-page, up to 200 frames), BMP, WebP, GIF

Share links work as-is: Google Drive files and Google Docs/Slides/Sheets shared as "Anyone with the link" (exported as Office files), Dropbox and GitHub.

### How to use it

1. Paste one or more document URLs, or upload files.
2. Optionally set a chunk size (for example 500 tokens) if you're loading a vector database.
3. Run. Each document becomes one row in the dataset with its Markdown and metadata.

#### Input example

```json
{
  "urls": [
    { "url": "https://arxiv.org/pdf/1706.03762" },
    { "url": "https://example.com/handbook.docx" }
  ],
  "ocrMode": "auto",
  "chunkSizeTokens": 500,
  "chunkOverlapTokens": 50
}
```

#### Output example

```json
{
  "url": "https://example.com/q2-sales-report.pdf",
  "status": "success",
  "fileType": "pdf",
  "title": "Quarterly Sales Report",
  "pageCount": 1,
  "pagesProcessed": 1,
  "ocrPageCount": 0,
  "tableCount": 1,
  "tokenEstimate": 240,
  "markdown": "# Quarterly Sales Report\n\n# Summary\n\nRevenue grew in every region...\n\n| Region | Q1 revenue | Q2 revenue | Growth |\n| --- | --- | --- | --- |\n| Ontario | $1.2M | $1.5M | 25% |",
  "chunks": [
    {
      "chunkIndex": 1,
      "text": "### Regional breakdown\n\n| Region | Q1 revenue | ...",
      "section": "Summary > Regional breakdown",
      "pageStart": 1,
      "pageEnd": 1,
      "tokenEstimate": 50
    }
  ],
  "warnings": []
}
```

Every input gets a row, and its `status` is one of:

- `success`: converted and charged.
- `failed`: not charged. The `error` explains why in plain English, for example *"The PDF is password-protected. Provide the password in the pdfPassword input."*
- `no_text`: the file had no extractable text. Not charged.
- `skipped`: not processed because the run reached the maximum cost you set.

Very large documents keep their Markdown (and chunks) in the run's key-value store, linked from `markdownFileUrl` / `chunksFileUrl`, because a dataset row is limited to about 9 MB.

### Use it from code, automations or AI agents

**HTTP API**: one request returns the results:

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~document-to-markdown/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":[{"url":"https://arxiv.org/pdf/1706.03762"}]}'
```

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("YOUR_USERNAME/document-to-markdown").call(run_input={
    "urls": [{"url": "https://arxiv.org/pdf/1706.03762"}],
    "chunkSizeTokens": 500,
})
for doc in client.dataset(run["defaultDatasetId"]).iterate_items():
    if doc["status"] == "success":
        print(doc["title"], doc["tokenEstimate"])
    else:
        print(doc["url"], doc["status"], doc.get("error"))
```

**AI agents (MCP)**: add this Actor to Claude, Cursor or any MCP client through the Apify MCP server (`https://mcp.apify.com`), and your agent can read any PDF or Office file it finds.

**Vector databases**: set **Chunk output** to *One dataset item per chunk* and connect the dataset to the Pinecone, Qdrant or other vector database integrations.

**Zapier, Make, n8n**: use the Apify integration and map the `markdown` field.

### Pricing

Pay per event: you only pay for what converts.

| Event | Price |
|---|---|
| Document converted | $0.002 |
| Page processed | $0.0001 |
| Page OCR (scanned pages only) | $0.004 |

Each run also has Apify's standard start fee of $0.00005 per GB of memory (1 GB by default).

Examples:

| Document | Cost |
|---|---|
| 10-page text PDF | $0.003 |
| 100-page report | $0.012 |
| 5-page scanned PDF | $0.0225 |
| Word document of about 6,000 characters | $0.0022 |

For formats without real pages, one "page" is 3,000 characters of output (PowerPoint counts slides). Empty spreadsheet cells are not counted. Documents that fail or contain no text are free.

**Maximum cost per run** is always respected. Near the limit, the Actor converts one document at a time so it can stop exactly. Documents it can't afford are listed with status `skipped`, and a document cut short says how many pages were included.

### Benchmark against other PDF Actors in Apify Store (21 September 2026)

Five public PDFs were run through this Actor and five other PDF-to-text Actors from Apify Store, using the same URLs for every tool, and scored against an answer key written from the page images. Each check is an exact text match on the tool's Markdown (or its plain text, if it has no Markdown) after dropping case, accents, spacing and punctuation. Formatting differences cost nothing; only missing, garbled or out-of-order text fails. The French check keeps accents, because that is what it tests.

| Test (what it measures) | **This Actor** | Actor A | Actor B | Actor C | Actor D | Actor E |
|---|---|---|---|---|---|---|
| 25-column statistics table: rows kept intact (5 rows) | **5/5** | 5/5 | 5/5 | 5/5 | 0/5 | 0/5 |
| 3-column Federal Register: sentences in reading order, including across column and page breaks (8) | **8/8** | 0/8 | 0/8 | 5/8 | 0/8 | 3/8 |
| Scanned page: sentences recovered by OCR (8) | **8/8** | 8/8 | 5/8 | 0/8 | 8/8 | 0/8 |
| 19-page report: sentences (2) and table rows (2) | **2/2 and 2/2** | 2/2 and 2/2 | 2/2 and 2/2 | 2/2 and 2/2 | 2/2 and 2/2 | 2/2 and 2/2 |
| French accents preserved (4 sentences) | **4/4** | 4/4 | 4/4 | 4/4 | 3/4 | 4/4 |
| Running headers and footers left in the text (46 on these pages; lower is better) | **0** | 46 | 46 | 46 | 46 | 22 |
| **Cost for all five documents (38 pages)** | **$0.018** | $0.033 | $0.045 | $0.019 | $0.013 | $0.120 |

The typical failures: on the three-column document, most tools mix lines from neighbouring columns into the same sentence. On the statistics table, some tools list every row label first and all the numbers after, or cut rows short. Some can't read scanned pages at all. And every other tool leaves some or all of the running headers and footers in the text, so a sentence that crosses a page break gets a footer line in the middle of it.

Test files (pinned commits, so anyone can repeat the test with any tool): [NICS background checks](https://raw.githubusercontent.com/jsvine/pdfplumber/4c64b92d5caccd71c645e98e0fabb0c4dba7ff45/tests/pdfs/nics-background-checks-2015-11.pdf) · [Federal Register 2020-17221](https://raw.githubusercontent.com/jsvine/pdfplumber/4c64b92d5caccd71c645e98e0fabb0c4dba7ff45/tests/pdfs/federal-register-2020-17221.pdf) · [scanned LinnSequencer page](https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/64d999aea85a672b51d83747f0b567907d7b38bd/tests/resources/linn.pdf) · [National Hydro Network data model](https://raw.githubusercontent.com/py-pdf/pypdf/f0b34a2879173df71467a889ce848e7064bbfb34/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf) · [French meeting minutes](https://raw.githubusercontent.com/jsvine/pdfplumber/4c64b92d5caccd71c645e98e0fabb0c4dba7ff45/tests/pdfs/2023-06-20-PV.pdf)

The other Actors ran with OCR, tables and header removal switched on wherever they offer those options. Their costs are what each run was charged on 20–21 September 2026, and their prices and results may have changed since. This Actor's cost is at its current price.

### Input options

| Option | Default | What it does |
|---|---|---|
| `urls` / `files` | | Documents to convert |
| `ocrMode` | `auto` | `auto` OCRs only pages without usable text, `force` OCRs every page, `off` never OCRs |
| `ocrLanguages` | `eng` | Tesseract languages, e.g. `eng+fra`. Installed: eng, fra, deu, spa, ita, por, nld, pol, chi\_sim, jpn |
| `pageRange` | all | PDF pages to convert, e.g. `1-5, 8, 10-` |
| `maxPages` | 0 (no limit) | Cap pages per PDF |
| `extractTables` | `true` | Markdown tables from ruled PDF tables |
| `removeHeadersFooters` | `true` | Drop running headers, footers and page numbers |
| `includePageBreaks` | `false` | Insert `<!-- page N -->` markers for citations |
| `pdfPassword` | | Password for protected PDFs |
| `chunkSizeTokens` | 0 (off) | Heading-aware chunks of about this many tokens |
| `chunkOverlapTokens` | 50 | Overlap between chunks in the same section |
| `chunkOutput` | `nested` | `nested` in each document, or `separateItems` (one row per chunk) |
| `saveMarkdownFiles` | `false` | Also save a downloadable `.md` file per document |
| `httpHeaders` | | Headers for private documents, e.g. `Authorization` |
| `maxFileSizeMb` | 100 | Skip larger files (up to 200) |
| `maxConcurrency` | 3 | Documents converted in parallel |

### FAQ

**Does it handle multi-column documents?** Yes. It detects the gaps between 2–4 columns and reads each column in order, while titles, abstracts and headings that span the page stay in place. It was tested on academic papers and the three-column Federal Register.

**How accurate is OCR?** It uses Tesseract 5, which does well on clean scans at typical office resolution. Photos of documents, handwriting and very low-resolution scans will be less accurate.

**What if a scan has no readable text?** The document is returned with status `no_text` and isn't charged, and OCR pages without text are never billed.

**Are images, charts or equations described?** No. The Actor extracts text. Text inside images is read only when a page is OCR'd.

**Tables without borders?** Tables need ruling lines to become Markdown tables. The text of borderless tables is still extracted, as plain lines.

**Is my data stored?** Documents are processed in memory during your run. Results are stored in your own Apify dataset under your account's retention settings.

**Legacy .doc or .ppt files?** Save them as .docx, .pptx or PDF first.

### Changelog

- **1.0.1** (21 September 2026): tables ruled only every few rows now give one Markdown row per record; title pages that open with a licence notice or strapline now keep the real title as the document title. The page price is halved.
- **1.0**: first release, covering PDF (layout-aware, tables, OCR), Office, HTML, images, RAG chunking and share-link support.

# Actor input Schema

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

Links to the documents to convert: PDF, Word (.docx), PowerPoint (.pptx), Excel (.xlsx, .xls), EPUB, HTML, CSV, JSON, text or images (PNG, JPG, TIFF). Google Drive, Google Docs, Dropbox and GitHub share links are converted to direct downloads automatically.

## `files` (type: `array`):

Upload documents from your computer instead of (or in addition to) URLs.

## `ocrMode` (type: `string`):

Auto: OCR only pages without a usable text layer (scans, photos, garbled fonts). Force: OCR every page, useful when a PDF's embedded text is wrong. Off: never OCR. OCR pages are billed separately.

## `ocrLanguages` (type: `string`):

Tesseract language codes joined with +, for example eng, fra, deu, spa, ita, por, nld, pol, eng+fra. Installed: eng, fra, deu, spa, ita, por, nld, pol, chi\_sim, jpn.

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

Only convert these pages, for example "1-5, 8, 10-". Leave empty for all pages.

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

Stop after this many pages of each PDF. 0 means no limit.

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

Detect ruled tables in PDFs and output them as Markdown tables.

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

Drops text repeated in page margins, such as journal names and page numbers, so it doesn't pollute your text.

## `includePageBreaks` (type: `boolean`):

Insert  comments so you can cite page numbers.

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

Password for protected PDFs (applied to every PDF in this run).

## `chunkSizeTokens` (type: `integer`):

Split each document into heading-aware chunks of about this many tokens for embeddings and RAG. 0 turns chunking off. Tokens are estimated at 4 characters each.

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

How much text consecutive chunks share, so answers that span a boundary aren't lost.

## `chunkOutput` (type: `string`):

Nested: chunks inside each document's result. Separate items: one dataset row per chunk, ready for Pinecone, Qdrant or other vector database integrations.

## `saveMarkdownFiles` (type: `boolean`):

Also save each result as a Markdown file in the run's key-value store and return its download link. Very large results are always saved this way.

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

Skip files larger than this.

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

How many documents to download and convert at the same time.

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

Extra headers for downloading private documents, for example {"Authorization": "Bearer ..."}.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://arxiv.org/pdf/1706.03762"
    }
  ],
  "ocrMode": "auto",
  "ocrLanguages": "eng",
  "maxPages": 0,
  "extractTables": true,
  "removeHeadersFooters": true,
  "includePageBreaks": false,
  "chunkSizeTokens": 0,
  "chunkOverlapTokens": 50,
  "chunkOutput": "nested",
  "saveMarkdownFiles": false,
  "maxFileSizeMb": 100,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

One row per input document: the Markdown, title, page, table and OCR counts, token estimate and RAG chunks.

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

Full .md files, saved when saveMarkdownFiles is on or when a document is too big for one dataset row.

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

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

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

```

## MCP server setup

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