# Document to Markdown — PDF, Word, PowerPoint, Excel, OCR (`saliga/document-to-markdown`) Actor

Convert PDF, DOCX, PPTX, XLSX, HTML, CSV, EPUB and images to clean Markdown for LLMs and RAG. Scanned pages are read with OCR. $2 per 1,000 pages.

- **URL**: https://apify.com/saliga/document-to-markdown.md
- **Developed by:** [Salih Küçükkayış](https://apify.com/saliga) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 pages

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

Convert documents to clean Markdown for LLMs, RAG pipelines and AI agents: PDF, Word (DOCX), PowerPoint (PPTX), Excel (XLSX), HTML, CSV, JSON, XML, EPUB, text and images. Give it file URLs, get one Markdown result per file: headings, paragraphs, lists and **tables as real Markdown tables**. Scanned PDF pages and images are read with OCR automatically, and the output can come pre-split into **RAG chunks** with page numbers.

**$2 per 1,000 pages** ($1.80, $1.60 and $1.40 on the Starter, Scale and Business plans). **$5 per 1,000 pages read with OCR** ($4.50, $4.00, $3.50). Files that fail are free. No subscription, no platform usage fees. (Plus a tiny start fee of $0.00005 per GB of run memory: $0.0001 per run at the default 2 GB.)

### Quick start

Press **Start** with the example input. It converts a sample PDF and a sample Word file:

```json
{
  "urls": [
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.pdf",
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.docx"
  ]
}
```

Each file becomes one dataset row (trimmed here):

```json
{
  "url": "https://.../test.pdf",
  "fileName": "test.pdf",
  "format": "pdf",
  "createdAt": "2023-10-05T00:07:56",
  "totalPages": 1,
  "pages": 1,
  "ocrPages": 0,
  "tables": 0,
  "wordCount": 774,
  "chars": 5191,
  "markdown": "<!-- page 1 -->\n\n1\n\n### Introduction\n\nLarge language models (LLMs) are becoming a crucial building block ..."
}
```

### What it does

- **PDF**: the text layer page by page, with wrapped lines joined into paragraphs, hyphenation undone, bullet lists kept and headings detected from the font size (`#`, `##`, `###`). `<!-- page N -->` markers let you cite page numbers.
- **PDF tables**: tables with lines between cells and borderless tables (columns aligned by spacing, as in most reports and financial statements) become Markdown tables in their place in the text, with the caption above. Rows that a PDF packs into one cell are split back into rows, and the table's text is not repeated as loose numbers. Typically under a second per page, on CPU.
- **RAG chunks** (optional): set *RAG chunk size* and each result also gets `chunks`, each with its text, page range and nearest heading. Paragraphs and tables are not cut in the middle; big tables are split between rows with the header repeated.
- **Metadata**: title, author and creation date from PDFs and Office files, plus page, table and word counts.
- **Scanned PDFs and images**: pages without a text layer are read with Tesseract OCR and rebuilt as Markdown: paragraphs instead of broken lines, bullet lists as lists, large lines as headings. Choose the language (`eng`, `deu`, `fra`, `spa`, `ita`, `por`, `nld`, `tur`, `pol`, `swe`, or several with `+`).
- **Word**: headings, lists, tables and links as Markdown.
- **PowerPoint**: one section per slide with its title, text, tables and notes.
- **Excel**: one Markdown table per sheet.
- **HTML, CSV, JSON, XML, EPUB, TXT, MD**: converted or kept as readable Markdown.
- **RAG friendly**: turn on *One result per page* to get one item per PDF page, ready for chunking and embedding.

The file type is detected from the file itself, so links without an extension work too.

#### Example: a table from a PDF report

From [this NREL report page](https://raw.githubusercontent.com/camelot-dev/camelot/master/tests/files/foo.pdf), where plain text extraction gives a column of loose numbers:

```markdown
Table 2-1. Simulated fuel savings from isolated cycle improvements

| Cycle Name | KI (1/km) | Distance (mi) | Percent Fuel Savings |  |  |  |
|---|---|---|---|---|---|---|
|  |  |  | Improved Speed | Decreased Accel | Eliminate Stops | Decreased Idle |
| 2012_2 | 3.30 | 1.3 | 5.9% | 9.5% | 29.2% | 17.4% |
| 2145_1 | 0.68 | 11.2 | 2.4% | 0.1% | 9.5% | 2.7% |
| 4234_1 | 0.59 | 58.7 | 8.5% | 1.3% | 8.5% | 3.3% |
```

### Input

| Field | Meaning |
|---|---|
| `urls` | Links to the files. Public http(s) URLs, up to the size limit. The same link twice is converted once. |
| `extractTables` | Output PDF tables as Markdown tables (default on). |
| `ocr` | `auto` (default): OCR a PDF page only when it has no text but has an image. `always`: OCR every PDF page. `never`: text layer only. |
| `ocrLanguage` | Language of scanned text, for example `eng` or `eng+deu`. |
| `maxPages` | Stop after this many pages of each PDF or image (0 = all). Caps the cost of long files. |
| `splitByPage` | One result per PDF page instead of one per file. |
| `pageMarkers` | Insert `<!-- page N -->` before each PDF page (default on). |
| `chunkSize`, `chunkOverlap` | Also return RAG chunks of about this many characters (0 = off), with this much overlap (default 200). |
| `requestHeaders` | Extra HTTP headers, for example `{"Authorization": "Bearer …"}` for files behind a token. Sent only to the host of each URL, never to a redirect on another host. |
| `maxFileSizeMb` | Skip files larger than this (default 100 MB, up to 500). |
| `maxConcurrency` | Files converted in parallel (default 3, up to 10). |

### Output

| Field | Meaning |
|---|---|
| `url`, `finalUrl` | The link you gave, and where it redirected to (if it did) |
| `fileName`, `format` | File name and detected type |
| `title`, `author`, `createdAt` | Document metadata, when the file has it |
| `totalPages`, `pages` | Pages in the PDF, and pages converted (and charged) |
| `ocrPages` | Pages read with OCR |
| `tables` | Tables found and output as Markdown tables |
| `wordCount` | Words in the Markdown |
| `chunks` | With *RAG chunk size*: `index`, `text`, `chars`, `pageStart`, `pageEnd`, `heading` |
| `unit` | For Office files: `slide`, `sheet` or `page` (see pricing) |
| `markdown`, `chars` | The Markdown and its length |
| `markdownUrl`, `markdownTruncated` | For very long documents (over 1.5 million characters) the full Markdown is saved as a file and linked here |
| `page`, `ocr` | With *One result per page*: the page number and whether it was read with OCR |
| `stoppedAtSpendingLimit` | `true` if the run's spending limit was reached in this file |
| `error` | Why a file failed (not found, too large, password-protected, unsupported type) |

### What counts as a page

| File | One billable page is |
|---|---|
| PDF | one page (OCR pages are charged as OCR pages instead) |
| Image | one image, or one frame of a multi-page TIFF (always OCR) |
| PowerPoint | one slide |
| Excel | one sheet |
| Word, HTML, EPUB, CSV, JSON, XML, text | 3,000 characters of output, at least one |

### What it costs

| Run | Price (no discount) |
|---|---|
| 100 PDFs of 10 pages with a text layer | $2.00 |
| 1,000 slides | $2.00 |
| 200 scanned pages (OCR) | $1.00 |
| A 300-page report, capped with `maxPages: 50` | $0.10 |

### Good to know

- No per-run file limits on any Apify plan, including the free plan: convert as many files as your budget allows.
- You only pay for pages you get. Failed downloads, unsupported files and password-protected PDFs are free.
- The run stops cleanly at your spending limit, and returns the pages that were paid for.
- Private and internal addresses (localhost, 10.x, 192.168.x, cloud metadata) are blocked, including every redirect.
- Files behind a login are not supported: the link must download the file directly.
- Borderless tables are found where numbers line up in columns; tables of mostly long text without lines come out as paragraphs. Merged header cells are left empty in the Markdown table. Tables inside scanned pages come out as text.
- Multi-column pages come out as readable text in reading order, not as a pixel-perfect copy.
- Failed downloads (connection errors, HTTP 429 and 5xx) are retried twice before the file is reported as failed.
- You are responsible for converting files you have the right to use.

### FAQ

**Can I use it in a RAG pipeline?** Yes. Set *RAG chunk size* (for example 1500) and embed each item's `chunks[].text`, keeping `url`, `pageStart` and `heading` as metadata for citations. Chunking is free: you pay for pages, not chunks.

**Are tables kept?** Yes, PDF tables (with or without lines) and all tables in Word, PowerPoint, Excel and HTML come out as Markdown tables. Set `extractTables` to false if you prefer plain text.

**How do I convert files behind a login or token?** Use a pre-signed URL (S3, Google Cloud Storage, Azure) or pass the token in `requestHeaders`.

**How do I convert files I have locally?** Upload them anywhere that gives a direct download link (for example an Apify key-value store, S3 or a public bucket) and pass the links.

**Can an AI agent use it?** Yes, through the Apify MCP server: the agent passes file URLs and gets Markdown back.

**Can I run it from Make, Zapier, n8n or my own code?** Yes, like any Apify Actor: through Apify's integrations or the Apify API.

### More from this developer

- [Bulk HTTP Requests](https://apify.com/saliga/bulk-http-requests): call any API once for every row of a CSV, JSON list or Apify dataset.
- [Website Screenshot + Change Detection](https://apify.com/saliga/website-screenshot-change-detection): screenshots of many URLs on desktop and mobile, with visual change detection between runs.
- [Keyword Search Volume](https://apify.com/saliga/keyword-search-volume): Google search volume, CPC and trends for a keyword list, in up to 20 countries in one run.
- [Audio to Text](https://apify.com/saliga/audio-to-text): audio, video and podcast feeds to timestamped text, SRT/VTT subtitles and RAG chunks with Whisper, $5 per 1,000 audio minutes.
- [Video to Scenes](https://apify.com/saliga/video-to-scenes): scene cuts, one keyframe per scene, a contact sheet and the transcript of each scene, $2 per 1,000 frames.
- [Career Site Jobs](https://apify.com/saliga/career-site-jobs): open jobs from Greenhouse, Lever, Ashby, Workable, Recruitee and Workday career sites, with a new-jobs monitor and a hiring summary per company, $1 per 1,000 jobs.

Questions or a missing feature: open an issue on the Actor page.

# Actor input Schema

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

Links to the files to convert: PDF, DOCX, PPTX, XLSX, HTML, CSV, JSON, XML, TXT, MD, EPUB or an image (PNG, JPEG, TIFF, WebP). One result per file. The type is detected from the file itself.

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

Detect tables in PDFs, with or without lines between cells, and output them as Markdown tables in place, with the caption above. Turn off to get their text as plain paragraphs.

## `ocr` (type: `string`):

Auto: read a PDF page with OCR only when it has no text layer but has an image (scans, photos). Always: OCR every PDF page. Never: text layer only. Images are always read with OCR.

## `ocrLanguage` (type: `string`):

Language of the scanned text. Combine with +, for example eng+deu. Available: eng, deu, fra, spa, ita, por, nld, tur, pol, swe.

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

Stop after this many pages of each PDF or image file (0 = all pages). Useful to cap the cost of very long documents.

## `splitByPage` (type: `boolean`):

For PDFs and images, output one dataset item per page instead of one per file. Handy for chunking in RAG pipelines.

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

Insert  before each PDF page so you can cite page numbers.

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

Also return the Markdown split into chunks of about this many characters (0 = off). Paragraphs and tables are not cut unless they are larger than a chunk; big tables are split between rows with the header repeated. Try 1500.

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

Characters of the previous chunk repeated at the start of the next one, cut at a word boundary (at most half the chunk size).

## `requestHeaders` (type: `object`):

Extra HTTP headers for downloading, for example {"Authorization": "Bearer …"} for files behind a token. Sent only to the host of each URL, never to a redirect on another host. They are stored with the run input, so prefer short-lived tokens or pre-signed URLs.

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

Files larger than this are skipped with an error.

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

How many files are downloaded and converted in parallel.

## Actor input object example

```json
{
  "urls": [
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.pdf",
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.docx"
  ],
  "extractTables": true,
  "ocr": "auto",
  "ocrLanguage": "eng",
  "maxPages": 0,
  "splitByPage": false,
  "pageMarkers": true,
  "chunkSize": 0,
  "chunkOverlap": 200,
  "requestHeaders": {},
  "maxFileSizeMb": 100,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

One row per file: type, pages, OCR pages, Markdown and any error.

## `fullResults` (type: `string`):

Every field, including total pages, page numbers when split by page and links to very long Markdown files.

# 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://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.pdf",
        "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.docx"
    ],
    "requestHeaders": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("saliga/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": [
        "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.pdf",
        "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.docx",
    ],
    "requestHeaders": {},
}

# Run the Actor and wait for it to finish
run = client.actor("saliga/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": [
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.pdf",
    "https://raw.githubusercontent.com/microsoft/markitdown/main/packages/markitdown/tests/test_files/test.docx"
  ],
  "requestHeaders": {}
}' |
apify call saliga/document-to-markdown --silent --output-dataset

```

## MCP server setup

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