# Document to Markdown: PDF, DOCX & HTML for RAG (`dodiabak/document-to-markdown`) Actor

Convert PDF, Word (DOCX), HTML pages, CSV and text into clean Markdown + JSON for LLMs and RAG: headings, tables, links, metadata and ready-to-embed chunks with heading paths and page numbers. URLs, Drive/Dropbox links or base64. No OCR, no AI cost.

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

## Pricing

from $1.00 / 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.

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 & HTML for RAG

**Turn PDFs, Word documents, web pages, CSV and text files into clean Markdown and structured JSON that LLMs, AI agents and RAG pipelines can use right away.** You get headings, paragraphs, tables (as Markdown and as JSON rows), links, metadata, word and token counts, and optional ready-to-embed chunks with section paths and page numbers.

It's fast and predictable: no OCR, no LLM calls, no browser. A 300-page PDF converts in about 5 seconds. You pay **$0.001 per page**.

### What does it do?

| Input | What you get |
| --- | --- |
| **PDF** (URL, Drive/Dropbox link or base64) | Markdown with `#` headings detected from font size and boldness, paragraphs re-flowed across line breaks, hyphenated words re-joined, bullet and numbered lists, **tables rebuilt from column-aligned text**, `<!-- page N -->` markers, title/author/dates, and a `needsOcr` flag for scanned PDFs. Password-protected PDFs work if you pass `password`. |
| **Word DOCX** | Headings, bold/italic, bullet and numbered lists, tables, links, plus title and author from the document properties. Google Docs links are exported as DOCX automatically. |
| **HTML / web page** | Main content only (Mozilla Readability, the engine behind Firefox Reader View): navigation, footers, cookie banners and ads removed. GFM tables, code blocks, lists, absolute links and images. Page metadata: title, author, description, publish date, language, canonical URL. |
| **CSV / TSV** | A Markdown table plus JSON rows and row/column counts. The delimiter is detected automatically. |
| **Markdown / plain text** | Normalized and chunked. |

Every document also gets a heading **outline**, **stats** (pages, characters, words, approximate tokens, tables) and clear **warnings** (scanned pages, truncated files, skipped rotated text).

### Why use it?

- **Better RAG answers.** Chunks follow the document structure: they break at headings and paragraphs, never in the middle of a table or code block, and each chunk carries its `headingPath` (for example `["Annual Report", "Risk Factors"]`) and `page` so answers can cite their source.
- **Made for AI agents.** One call turns a link into text the model can read. Output fields are stable and documented, and failures come back as error codes (`HTTP_ERROR`, `PASSWORD_REQUIRED`, `FILE_TOO_LARGE`, `UNSUPPORTED_TYPE`…) instead of crashing the run.
- **Cheap and fast.** Text extraction runs locally with pdf.js and mammoth, so there's no GPU, OCR or AI bill.
- **Bulk.** Up to 1,000 documents per run, 3 in parallel by default.

### How to use it

1. Add your documents to **Documents**. Each one is a URL, or an object with `base64` + `fileName`, `html`, `text` or `markdown`.
2. Choose an **Output mode**: *One item per document* for Markdown files, or *One item per chunk* to load straight into a vector database (Pinecone, Qdrant, Weaviate, pgvector, Chroma…).
3. Click **Start**. Download the results as JSON, CSV or Excel, use the API, or turn on **Save .md files** to get a download link for each Markdown file.

#### Input example

```json
{
  "sources": [
    { "url": "https://arxiv.org/pdf/1706.03762v7" },
    { "url": "https://drive.google.com/file/d/FILE_ID/view?usp=sharing", "id": "contract-17" },
    { "base64": "UEsDBBQABgAIAAAAIQ…", "fileName": "policy.docx" },
    { "url": "https://example.com/locked.pdf", "password": "1234" },
    { "html": "<h1>Price list</h1><table>…</table>" }
  ],
  "outputMode": "document",
  "includeChunks": true,
  "chunkSize": 2000,
  "chunkOverlap": 200
}
```

#### Output example (document mode, shortened)

```json
{
  "sourceIndex": 0,
  "status": "ok",
  "url": "https://arxiv.org/pdf/1706.03762v7",
  "fileName": "1706.03762v7.pdf",
  "type": "pdf",
  "title": "Attention Is All You Need",
  "markdown": "<!-- page 1 -->\n\n# Attention Is All You Need\n\n…\n\n### Abstract\n\nThe dominant sequence transduction models…",
  "headings": [{ "level": 1, "text": "Attention Is All You Need" }, { "level": 3, "text": "1 Introduction" }],
  "tables": [{ "page": 6, "method": "heuristic", "rows": [["Self-Attention", "O(n2 · d)", "O(1)", "O(1)"], ["Recurrent", "O(n · d2)", "O(n)", "O(n)"]] }],
  "links": [{ "text": "https://github.com/tensorflow/tensor2tensor", "url": "https://github.com/tensorflow/tensor2tensor" }],
  "metadata": { "title": null, "author": null, "producer": "pdfTeX-1.40.25", "creationDate": "2024-04-10T21:11:43Z" },
  "stats": { "pages": 15, "pagesProcessed": 15, "billedPages": 15, "characters": 38203, "words": 6034, "approxTokens": 9747, "tables": 5, "headings": 15 },
  "needsOcr": false,
  "warnings": ["283 rotated/vertical text fragment(s) (e.g. margin stamps) were skipped."]
}
```

#### Output example (chunks mode: one item per chunk)

```json
{
  "sourceIndex": 0,
  "title": "Attention Is All You Need",
  "chunkIndex": 5,
  "chunkCount": 27,
  "headingPath": ["Attention Is All You Need", "3 Model Architecture"],
  "page": 2,
  "text": "### 3 Model Architecture\n\nMost competitive neural sequence transduction models have an encoder-decoder structure…",
  "chars": 1437,
  "approxTokens": 360
}
```

A document that can't be converted returns `{"status": "error", "errorCode": "HTTP_ERROR", "error": "Download failed: HTTP 404 …"}` and isn't charged.

### Input options

| Field | Default | Description |
| --- | --- | --- |
| `sources` | *(required)* | URLs or objects (`url`, `base64` + `fileName`, `html`, `text`, `markdown`). Optional per source: `type`, `password`, `id`. |
| `outputMode` | `document` | `document` (one item per file) or `chunks` (one item per chunk). |
| `includeChunks` | `false` | Add a `chunks` array to document items. |
| `chunkSize` / `chunkOverlap` | `2000` / `200` | In characters (about 4 per token). |
| `mainContentOnly` | `true` | Web pages: keep only the main article. |
| `linkStyle` | `inline` | `text` drops URLs from the Markdown to save tokens (they stay in `links`). |
| `detectTables` | `true` | PDF table reconstruction. |
| `pageMarkers` | `true` | `<!-- page N -->` between PDF pages. |
| `maxPages` | `300` | PDF pages converted (and billed) per document. |
| `includeTables` / `includeLinks` / `includeText` | `true` / `true` / `false` | Extra JSON fields. |
| `saveMarkdownFiles` | `false` | Save `doc-0001.md`… to the key-value store and return `markdownUrl`. |
| `maxFileSizeMb` | `25` | Download size limit (up to 100). |
| `requestTimeoutSecs` | `60` | Download timeout per URL. |
| `concurrency` | `3` | Documents processed in parallel. |

### Pricing

**$0.001 per page** ($1 per 1,000 pages). There's no subscription and no charge for failed documents.

- **PDF:** one page = one PDF page converted (capped by `maxPages`).
- **DOCX, HTML, CSV, text:** one page = every 3,000 characters of extracted text (minimum 1). A typical web article is 1–4 pages.

Example: 100 ten-page PDFs cost $1.00. The Actor respects your run's maximum charge and stops cleanly when it's reached.

### Use with AI agents (MCP) and the API

**MCP:** add this Actor to Claude Desktop, Cursor, VS Code or any MCP client through the Apify MCP server:

```
https://mcp.apify.com?tools=dodiabak/document-to-markdown
```

The agent can then call it with `{"sources": ["https://…/file.pdf"]}` and read the `markdown` field.

**API (synchronous, returns the items):**

```bash
curl -X POST "https://api.apify.com/v2/acts/dodiabak~document-to-markdown/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sources": ["https://arxiv.org/pdf/1706.03762v7"], "outputMode": "chunks"}'
```

**Python:**

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("dodiabak/document-to-markdown").call(run_input={"sources": ["https://example.com/report.pdf"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["stats"]["pages"], item["markdown"][:200])
```

It also works with LangChain, LlamaIndex, n8n, Make and Zapier through the Apify integrations.

### FAQ

**Does it do OCR on scanned PDFs or images?** No. It reads the PDF text layer, which most PDFs from Word, LaTeX, browsers and reporting tools have. Scanned PDFs return `needsOcr: true` with a warning so your pipeline can send them to an OCR service. Image files are rejected with `UNSUPPORTED_TYPE`.

**How good is table extraction?** Tables in DOCX and HTML are exact. PDF tables are rebuilt from column positions (`"method": "heuristic"`). Simple tables with one line per row work well. Tables with merged cells or wrapped multi-line cells may come out partly as plain text.

**Which files are supported?** PDF, DOCX, HTML, CSV/TSV, Markdown and TXT. Legacy `.doc`, `.xls`, `.pptx` and `.xlsx` aren't supported yet; save them as PDF or DOCX.

**Can I use private files?** Yes. Send the file as `base64`, or share a Google Drive, Docs or Dropbox link as "Anyone with the link". Files are processed in memory, stored only in your own run storage, and never sent to a third party.

**Does it crawl websites?** No. It fetches exactly the URLs you give it, one request per document. To convert a whole site, run a crawler first and pass its URLs or HTML here.

**What about multi-column layouts?** Text is read line by line from top to bottom, so two-column academic papers can have columns interleaved on some lines. Single-column documents (most reports, contracts, invoices, manuals) convert cleanly.

### Limitations

- No OCR and no image descriptions. Formulas come out as plain text.
- PDF heading levels are inferred from font size, so very uniformly styled PDFs may have few or no headings.
- Max 1,000 documents per run and 100 MB per file.

### Tech and sources

[pdf.js](https://github.com/mozilla/pdf.js) (Mozilla, Apache-2.0), [mammoth](https://github.com/mwilliamson/mammoth.js) (BSD-2), [Readability](https://github.com/mozilla/readability) (Mozilla, Apache-2.0), [Turndown](https://github.com/mixmark-io/turndown) + GFM plugin (MIT), [linkedom](https://github.com/WebReflection/linkedom) (ISC).

Questions or a document that converts badly? Open an issue on the Actor's **Issues** tab and include a sample file if you can.

# Changelog

This Actor's version history is a separate document: https://apify.com/dodiabak/document-to-markdown/changelog.md

# Actor input Schema

## `sources` (type: `array`):

Documents to convert. Each item is a URL string or an object: {"url": "https://…/file.pdf"}, {"base64": "…", "fileName": "report.docx"}, {"html": "<h1>…</h1>"}, {"text": "…"} or {"markdown": "…"}. Optional per item: "type" (pdf, docx, html, csv, markdown, text), "password" (encrypted PDF) and "id" (your own reference, copied to the output). Google Drive, Google Docs, Dropbox and GitHub share links are converted to direct downloads automatically. Max 1,000 per run.

## `outputMode` (type: `string`):

"document": one dataset item per document (Markdown, tables, links, metadata, optional chunks). "chunks": one dataset item per RAG chunk, ready to embed and load into a vector database.

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

In "document" mode, also add a "chunks" array to each item. Always on in "chunks" mode.

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

Target maximum chunk length in characters (about 4 characters per token). Chunks break at headings and paragraphs and never split a table row or code block unless a single block is too long.

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

Characters of the previous chunk repeated at the start of the next one (cut at a word boundary). The overlap resets at H1/H2 headings.

## `mainContentOnly` (type: `boolean`):

For HTML pages, keep only the main article (Mozilla Readability), removing navigation, footers, cookie banners and ads. Turn this off to convert the whole <body>.

## `linkStyle` (type: `string`):

"inline" keeps [text](url) links. "text" keeps only the link text: shorter, cleaner input for LLMs and embeddings (URLs stay available in the "links" array).

## `detectTables` (type: `boolean`):

Rebuild tables from column-aligned text in PDFs and output them as Markdown tables and JSON rows. This uses layout heuristics: complex tables (merged cells, ruled grids with wrapped text) may come out partly as text.

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

Insert  comments between PDF pages so chunks keep their page number (useful for citations).

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

Only the first N pages of each PDF are converted and billed.

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

Add a "tables" array (rows of cell strings) to document items.

## `includeLinks` (type: `boolean`):

Add a "links" array (text + absolute URL) to document items.

## `includeText` (type: `boolean`):

Also add a "text" field with plain text (no Markdown syntax).

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

Also save each document as a .md file in the run's key-value store and return its download URL as "markdownUrl".

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

Larger downloads are rejected with FILE_TOO_LARGE.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each URL download before failing it with FETCH_FAILED.

## `concurrency` (type: `integer`):

How many documents are processed at the same time. Use 1–2 for very large PDFs with low memory.

## Actor input object example

```json
{
  "sources": [
    {
      "url": "https://arxiv.org/pdf/1706.03762v7"
    },
    {
      "url": "https://en.wikipedia.org/wiki/Markdown"
    },
    {
      "html": "<h1>Price list</h1><p>Valid from <b>1 May</b>.</p><table><tr><th>Plan</th><th>Price</th></tr><tr><td>Basic</td><td>$9</td></tr><tr><td>Pro</td><td>$29</td></tr></table>",
      "id": "inline-html-example"
    }
  ],
  "outputMode": "document",
  "includeChunks": false,
  "chunkSize": 2000,
  "chunkOverlap": 200,
  "mainContentOnly": true,
  "linkStyle": "inline",
  "detectTables": true,
  "pageMarkers": true,
  "maxPages": 300,
  "includeTables": true,
  "includeLinks": true,
  "includeText": false,
  "saveMarkdownFiles": false,
  "maxFileSizeMb": 25,
  "requestTimeoutSecs": 60,
  "concurrency": 3
}
```

# Actor output Schema

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

No description

## `chunks` (type: `string`):

No description

## `files` (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 = {
    "sources": [
        {
            "url": "https://arxiv.org/pdf/1706.03762v7"
        },
        {
            "url": "https://en.wikipedia.org/wiki/Markdown"
        },
        {
            "html": "<h1>Price list</h1><p>Valid from <b>1 May</b>.</p><table><tr><th>Plan</th><th>Price</th></tr><tr><td>Basic</td><td>$9</td></tr><tr><td>Pro</td><td>$29</td></tr></table>",
            "id": "inline-html-example"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dodiabak/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 = { "sources": [
        { "url": "https://arxiv.org/pdf/1706.03762v7" },
        { "url": "https://en.wikipedia.org/wiki/Markdown" },
        {
            "html": "<h1>Price list</h1><p>Valid from <b>1 May</b>.</p><table><tr><th>Plan</th><th>Price</th></tr><tr><td>Basic</td><td>$9</td></tr><tr><td>Pro</td><td>$29</td></tr></table>",
            "id": "inline-html-example",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dodiabak/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 '{
  "sources": [
    {
      "url": "https://arxiv.org/pdf/1706.03762v7"
    },
    {
      "url": "https://en.wikipedia.org/wiki/Markdown"
    },
    {
      "html": "<h1>Price list</h1><p>Valid from <b>1 May</b>.</p><table><tr><th>Plan</th><th>Price</th></tr><tr><td>Basic</td><td>$9</td></tr><tr><td>Pro</td><td>$29</td></tr></table>",
      "id": "inline-html-example"
    }
  ]
}' |
apify call dodiabak/document-to-markdown --silent --output-dataset

```

## MCP server setup

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