# Document to Markdown: PDF, DOCX, XLSX, PPTX & OCR for AI (`terracotta_hauyne/document-to-markdown-for-ai`) Actor

Convert PDF (OCR for scans), DOCX, XLSX, PPTX, HTML, EML and images to clean Markdown for LLMs and RAG. Tables as Markdown, per-page text, token counts, heading-aware chunks. PDF text extractor: $4 per 1,000 files, OCR $10 per 1,000 pages.

- **URL**: https://apify.com/terracotta_hauyne/document-to-markdown-for-ai.md
- **Developed by:** [Kevin](https://apify.com/terracotta_hauyne) (community)
- **Categories:** AI, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 document processeds

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: PDF, DOCX, XLSX, PPTX & OCR for AI / RAG

**Convert PDF, Word, Excel, PowerPoint, HTML, e-mail and image files to clean Markdown** for ChatGPT, Claude, Gemini and RAG pipelines.
A **PDF text extractor with OCR**: it reads the text layer of a PDF first and runs **OCR only on the pages that have no text** (scans,
photographed pages), so you pay the OCR price only where it is needed. One Actor for PDF, DOCX, XLSX, PPTX, HTML, EML and images.

You get, per file: the whole **Markdown**, **pages\[]**, every **table as a Markdown table**, **metadata** (page count, OCR used,
word count, token estimate) and optional **heading-aware chunks\[]** for embeddings.

### Who is it for

- **AI / LLM developers** building RAG, chat-with-your-documents or agent tools who need Markdown instead of raw PDF text.
- **Data and ops teams** turning invoices, reports, contracts and spreadsheets into text that can be searched, summarized or fed to a model.
- **No-code users** (Make, Zapier, n8n) who want "file in, Markdown out" without running OCR or parsers themselves.

### Quick answers

- **How do I convert a PDF to Markdown?** Paste the link to the PDF into **Document URLs** and run. Result: `markdown`, per-page text, tables, metadata.
- **How do I OCR a scanned PDF?** Same input. Pages with no text layer are OCRed automatically (`ocr: auto`). Choose the language(s) in **OCR languages**.
- **Which formats?** PDF, DOCX, XLSX, PPTX, HTML, EML (e-mail), PNG, JPG, TIFF (multi-page), WebP, GIF, BMP, plus TXT, MD and CSV. The file content decides the type, not the extension.
- **How can I give it files?** Direct URLs, an uploaded file, records of an Apify key-value store, or base64 strings (for API and agent calls).
- **Price:** **$0.004 per converted file** ($4 per 1,000) plus **$0.010 per OCR page** ($10 per 1,000 pages). Files that fail are **not charged**. Apify also charges its small Actor-start fee.
- **Does it need an API key or an AI model?** No. It runs locally in the container (Tesseract OCR), calls no external AI service, and needs no secrets.
- **RAG chunks?** Switch on **Include RAG chunks**: text split at headings and paragraph ends, with heading path, page range and token estimate.
- **Cost control:** set Apify's maximum cost per run. The Actor stops when the budget is used up and never OCRs more pages than the budget pays for. `maxOcrPagesPerDocument` caps OCR per file.

### What you get

One dataset item per converted file (failed files are listed in the run summary, see below):

```json
{
  "documentIndex": 1,
  "source": "https://example.com/quarterly-report.pdf",
  "filename": "quarterly-report.pdf",
  "format": "pdf",
  "status": "ok",
  "markdown": "# Quarterly Report\n\nApify actors turn websites and files into structured data ...\n\n## Key figures\n\n- Revenue grew in Europe\n- Costs stayed flat\n\n| Region | Revenue | Growth |\n| --- | --- | --- |\n| Europe | 120 | 5% |\n| Asia | 80 | 9% |\n\n## Outlook\n\nSecond page text about the outlook for next year.",
  "metadata": {
    "title": "Quarterly Report", "author": "Test Author", "createdAt": "2026-11-05T11:32:21",
    "pageCount": 2, "ocrUsed": false, "ocrPages": 0,
    "wordCount": 47, "characterCount": 350, "tokenEstimate": 88, "tableCount": 1, "sizeBytes": 3064
  },
  "pages": [
    {"page": 1, "markdown": "# Quarterly Report\n\n...", "ocr": false, "wordCount": 37},
    {"page": 2, "markdown": "## Outlook\n\nSecond page text ...", "ocr": false, "wordCount": 10}
  ],
  "tables": [
    {"page": 1, "index": 1, "rows": 3, "columns": 3, "markdown": "| Region | Revenue | Growth |\n| --- | --- | --- |\n| Europe | 120 | 5% |\n| Asia | 80 | 9% |"}
  ],
  "chunks": [
    {"index": 0, "text": "# Quarterly Report\n\n...", "headings": ["Quarterly Report"], "pageStart": 1, "pageEnd": 2, "tokenEstimate": 88}
  ],
  "warnings": [],
  "markdownUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/001-quarterly-report.pdf.md",
  "processingSeconds": 0.21
}
```

- `markdownUrl` is the complete Markdown as a `.md` file in the run's key-value store (also listed in the **Output** tab). If an item would exceed Apify's 9 MB dataset limit, `chunks`, then `pages`, then `tables` are left out (with a warning) and `markdownUrl` still has everything.
- `pages[]`: PDF pages, spreadsheet sheets, slides, image frames. Word, HTML and e-mail files count as one page.
- `metadata.ocrConfidence` (0-100) appears when OCR ran. Below 60 the Actor adds a warning that the text needs a check.
- `tokenEstimate` is about 4 characters per token (use your own tokenizer for exact counts).
- The **run summary** (`OUTPUT` record) lists every file with status, error code and message for failed files, OCR pages and timings.

#### Failed files

A file that cannot be converted (bad link, wrong type, corrupt, password protected, too big, too many pages, blocked address) gets a row with `status: "failed"`, an `errorCode` and a clear `error` message in the **OUTPUT** record, and is **not charged**. If every file fails, the run fails with the first message. Error codes: `bad_url`, `blocked_url`, `dns_failed`, `http_error`, `fetch_failed`, `timeout`, `too_large`, `too_many_pages`, `empty`, `unreadable`, `password_protected`, `unsupported_format`, `ocr_disabled`, `ocr_unavailable`, `ocr_failed`, `ocr_timeout`, `not_found`, `limit_reached`, `internal_error`.

### How to use

1. Click **Start** with the prefilled example (a public-domain IRS form, 6 pages, about 3 seconds) or paste your own direct file links.
2. For private files use **Upload a file**, a **key-value store** (`sourceKeys`) or **base64 files** (`[{"filename": "a.pdf", "contentBase64": "..."}]`).
3. Optional: switch on **Include RAG chunks**, choose **OCR languages**, change limits.
4. Open the **Output** tab, or call the dataset API from your code.

API example (JavaScript):

```js
const run = await client.actor('<user>/document-to-markdown-for-ai').call({
  documentUrls: ['https://example.com/contract.pdf', 'https://example.com/prices.xlsx'],
  includeChunks: true, chunkSize: 600, ocrLanguages: ['eng', 'deu'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const doc of items) console.log(doc.filename, doc.metadata.tokenEstimate, doc.chunks.length);
```

### How each format is converted

| Format | Result |
|---|---|
| **PDF** | Text layer first (page by page). Tables found by ruling lines become Markdown tables; headings come from font size and bold; list bullets become `-` items; wrapped lines are joined into paragraphs. Pages with no text layer are OCRed. Metadata: title, author, dates. |
| **DOCX** | Headings (Title, Heading 1-6), paragraphs, bold/italic, bullet/numbered lists, tables, in document order. Embedded images are skipped. |
| **XLSX** | One Markdown table per visible sheet (hidden sheets skipped), cell values as shown by the last saved calculation, dates as ISO, a row cap (`maxSheetRows`, default 2,000) and a warning when cut. |
| **PPTX** | One section per slide: title, text in reading order, tables, speaker notes. Pictures are skipped. |
| **HTML** | Main content (`<main>` or `<article>`, else the body without nav/footer/aside), scripts and styles removed, links kept, tables extracted. |
| **EML** | Subject, From, To, Date, body (HTML preferred), list of attachments (attachments are listed, not converted: send them as separate files). |
| **Images** | OCR. Multi-page TIFF = one page per frame. Small images are enlarged before OCR for better accuracy. |
| **TXT / MD / CSV** | Text as is; CSV becomes a table (with the row cap). |

### Limits (all adjustable in the input)

| Setting | Default | Max |
|---|---|---|
| File size | 50 MB | 200 MB |
| Pages per document (PDF pages, slides, TIFF frames) | 100 | 2,000 |
| OCR pages per document | 50 | 2,000 |
| Rows per sheet | 2,000 | 200,000 |
| Documents per run | 100 | 10,000 |

Over a limit the file fails with a message that names the setting, and nothing is charged. Pages beyond `maxOcrPagesPerDocument` are not OCRed (and not charged): they are left empty and the file carries a warning.

### What it cannot do (honest list)

- **No handwriting.** OCR is Tesseract 5 for **printed** text. Photos of crumpled paper, tiny print and low-resolution scans lower the accuracy; the run reports a confidence score and warns below 60.
- **Complex layouts.** Multi-column pages, forms with boxes and magazine layouts are read in a simplified order (see the IRS Form W-9 example: the text is all there, but columns can interleave). Tables without ruling lines are not detected as tables and stay as text.
- **Scanned tables** are returned as OCR text, not as Markdown tables.
- **Old binary Office files** (.doc, .xls, .ppt) and RTF are not supported: save as .docx, .xlsx, .pptx first. Images inside Word and PowerPoint files are not OCRed.
- **Formulas and charts:** XLSX values come from the last calculation saved in the file; charts and shapes are ignored.
- **No web crawling.** It reads the file links you give it. Links to private networks and cloud metadata addresses are refused.
- **Languages for OCR:** English, German, French, Spanish, Italian, Portuguese, Dutch, Polish, Czech, Swedish, Russian, Turkish, Arabic, Hindi, Vietnamese, Japanese, Korean, Chinese (simplified, traditional). Pick every language that appears on the page.

### Speed and memory

Measured in the Docker image with the CPU limited like the Apify platform gives it (about 1 core per 4 GB of memory; see TESTING.md):

- **OCR:** about 2.6 s per dense A4 page (about 700 words) with 1 core / 4 GB, about 7 s with 0.5 core / 2 GB. Memory stays under 350 MB.
- **PDF with a text layer:** about 0.3 s per page; Word, Excel, PowerPoint, HTML: under 1 s per file.
- Default memory is 2048 MB (fine for most runs). For big OCR batches choose **4096 MB**: twice the CPU, so a run finishes about 2.5x faster.

### Safety

- URLs are checked before and after every redirect, and the connection goes to the address that was checked. Loopback, private, link-local, carrier-grade NAT, multicast, reserved and cloud metadata addresses, `localhost`, `.internal` and `.local` names are refused.
- Your files are processed in the run's container and stored only in the run's own key-value store and dataset. Nothing is sent to a third-party service.
- Office files are checked against zip bombs (more than 400 MB expanded is refused).

### Open-source components and licences

Components (licences checked at build time, versions pinned in `requirements.txt`):
Tesseract OCR 5 (Apache-2.0), pdfplumber and pdfminer.six (MIT), pypdf (BSD-3), pypdfium2 (Apache-2.0 / BSD-3, PDFium), python-docx, openpyxl and python-pptx (MIT), Beautiful Soup (MIT), lxml (BSD-3), markdownify (MIT), Pillow (HPND), httpx (BSD-3), Apify SDK (Apache-2.0). **No AGPL/GPL component** (PyMuPDF and pymupdf4llm were deliberately not used).

### FAQ

**Is there a free way to try it?** The prefilled input converts a public-domain PDF for a few cents of Apify credit.

**Why is my OCR text wrong?** Check `metadata.ocrConfidence` and `warnings`. Pick the right OCR languages, raise **OCR render resolution** to 300 for fine print, or send a better scan.

**My PDF has text but it looks like garbage.** Some PDFs embed fonts without a usable text map. Set **OCR mode** to `force` to OCR every page instead.

**Can I get only the Markdown?** Yes: turn off pages and tables; use `markdown` or the `.md` file at `markdownUrl`.

**Do you keep my files?** No. They live in the run's storage, which follows your Apify retention settings.

### Development

See TESTING.md (tests, measurements), PRICING.md (price and cost), PUBLISHING.md (release notes).

# Actor input Schema

## `documentUrls` (type: `array`):

Direct links to files: PDF, DOCX, XLSX, PPTX, HTML, EML, PNG, JPG, TIFF, WebP, TXT, CSV. The link must open the file without login. Links to private networks and cloud metadata addresses are blocked.

## `uploadedFile` (type: `string`):

Optional. Upload one file here (it is stored in Apify and read like a URL). For several private files use the key-value store or base64 fields below.

## `sourceKeyValueStoreId` (type: `string`):

Optional. Key-value store that holds your files, used with "Record keys". Leave empty to read the keys from this run's default store.

## `sourceKeys` (type: `array`):

Keys of binary file records in the key-value store above. Each record is one document. The key's file extension helps to detect the type, but the content decides.

## `base64Files` (type: `array`):

Files sent inline, for API and agent use: a list of objects like {"filename": "report.pdf", "contentBase64": "JVBERi0..."}. A data: URI is accepted as contentBase64 too.

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

auto reads the text layer of a PDF and uses OCR only for pages that have none (billed per OCR page). Images are always OCRed unless off. force OCRs every PDF page and replaces its text layer, for PDFs whose embedded text is garbage. Printed text only; no handwriting.

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

Languages of the scanned text (Tesseract). Pick every language that appears on the page; more languages are slightly slower. Printed text only.

## `ocrResolutionDpi` (type: `integer`):

Resolution at which PDF pages are rendered for OCR. 200 reads normal print (10 pt and up) well; use 300 for fine print. Higher is slower and uses more memory.

## `maxOcrPagesPerDocument` (type: `integer`):

Cost guard. Pages without text beyond this number are left empty and a warning is added.

## `includePages` (type: `boolean`):

Add the Markdown of each page (PDF page, sheet, slide) next to the full text. Turn off to halve the item size.

## `includeTables` (type: `boolean`):

Add every table as its own Markdown table with page, row and column counts.

## `includeChunks` (type: `boolean`):

Add chunks\[]: text split at headings and paragraph boundaries with the heading path, page range and token estimate, ready for an embedding model.

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

Target chunk size in estimated tokens (about 4 characters per token). A paragraph or table is never cut unless it is bigger than this.

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

Whole paragraphs from the end of the previous chunk repeated at the start of the next one, up to this many tokens. Must be smaller than the chunk size.

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

Larger files fail with a clear message and are not charged.

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

PDFs, slide decks and TIFFs with more pages fail with a clear message and are not charged.

## `maxSheetRows` (type: `integer`):

Spreadsheet and CSV rows (after the header) converted per sheet. Extra rows are cut and a warning is added.

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

Documents beyond this number are not attempted.

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

Maximum time to download one file.

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

Optional. Password for encrypted PDFs; used for every PDF in the run.

## Actor input object example

```json
{
  "documentUrls": [
    "https://example.com/report.pdf"
  ],
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "ocrResolutionDpi": 200,
  "maxOcrPagesPerDocument": 50,
  "includePages": true,
  "includeTables": true,
  "includeChunks": false,
  "chunkSize": 800,
  "chunkOverlap": 80,
  "maxFileSizeMb": 50,
  "maxPagesPerDocument": 100,
  "maxSheetRows": 2000,
  "maxDocuments": 100,
  "downloadTimeoutSecs": 120
}
```

# Actor output Schema

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

One item per converted file (default dataset): Markdown, pages, tables, metadata and optional chunks.

## `markdownFiles` (type: `string`):

The complete Markdown of every converted file as a .md record in the run's key-value store.

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

Per-file status including failed files with an error code and message, OCR page count and timings.

# 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 = {
    "documentUrls": [
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("terracotta_hauyne/document-to-markdown-for-ai").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 = { "documentUrls": ["https://www.irs.gov/pub/irs-pdf/fw9.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("terracotta_hauyne/document-to-markdown-for-ai").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 '{
  "documentUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call terracotta_hauyne/document-to-markdown-for-ai --silent --output-dataset

```

## MCP server setup

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

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/DFusLt5AEB4Y5rPGS/builds/OGgN73OA89LnG1WpH/openapi.json
