# PDF to Markdown: Word, Excel, PPTX, Tables & OCR (`verrr/document-to-markdown`) Actor

Convert PDF, Word, PowerPoint, Excel, HTML and text files to clean Markdown, page-numbered chunks and tables as JSON rows, ready for RAG and AI agents. Paste URLs or pick a dataset from any Actor. Scanned pages are read by OCR. Pay per document, plus a small fee per 100 PDF pages past the first 100.

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

## Pricing

from $3.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 to Markdown converter for RAG and AI agents: give it file URLs or a dataset of links, and get clean Markdown, page-numbered chunks and tables as JSON rows from PDF, Word, PowerPoint, Excel, HTML and text files. Scanned PDF pages are read by OCR.

### What data you get

One record per file:

- `url`: the file URL you gave.
- `format`: `pdf`, `docx`, `pptx`, `xlsx`, `html` or `text`, detected from the file itself, not from its extension.
- `title`: the document title, or the file name if it has none.
- `pageCount`: PDF pages, PPTX slides or XLSX sheets in the whole file. Null for formats with no pages.
- `markdown`: the full text as Markdown, with headings, lists, links and tables.
- `chunks`: pieces of up to 1,500 characters, each with `index`, `page` and `text`. A chunk never crosses a page, so you can cite the page. `page` is the PDF page, PPTX slide or XLSX sheet number, and null for Word, HTML and text.
- `tables`: each table with its `page`, `title` (the sheet name for XLSX, the caption for HTML), `headers` and `rows`. Rows are JSON objects keyed by header, and values are strings.
- `ocrPages`: how many pages gained new text from OCR, which is also how many `ocr-page` events this file was billed.
- `status`: `converted`, or `partial` if part of the file is missing from the output.
- `warnings`: why the status is `partial`. Empty otherwise.

### Input

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `urls` | array | no | `[]` | Public http or https links to files. |
| `datasetId` | string | no | none | A dataset of file links, for example the output of a scraper. Choose it in the Dataset field: a run can read only a dataset chosen there. |
| `urlField` | string | no | empty | The dataset field that holds the link. A dotted path reaches nested fields, and a field may hold a list of links. Left empty, common names are found in this order: pdfUrl, fileUrl, documentUrl, downloadUrl, pdf, file, document, attachment, url, link, href. |
| `ocr` | boolean | no | `true` | Read scanned PDF pages with OCR. |
| `maxPages` | integer | no | `200` | Only the first pages of a longer file are converted: PDF pages, PPTX slides or XLSX sheets. Up to 2,000. |
| `maxFileSizeMb` | integer | no | `50` | Larger files are skipped and not billed. Up to 200. |
| `maxItems` | integer | no | `1000` | The most files one run converts. Up to 100,000. |

An empty run converts a small built-in sample, which is billed like any other run. A link listed twice is converted and billed once.

### Pricing

Pay-Per-Event:

| Event | Price |
|---|---|
| `document-converted` | $0.003 per file ($3.00 per 1,000 files), covering the first 100 pages of a PDF |
| `extra-pages` | $0.003 per further block of 100 PDF pages delivered |
| `ocr-page` | $0.007 per page where OCR adds new text |
| `apify-actor-start` | $0.00005 per run |

Long PDFs cost more than short ones: a PDF up to 100 pages costs $0.003, 200 pages costs $0.006, and all 753 pages of one (with `maxPages` raised) cost $0.024. The default `maxPages` of 200 keeps a PDF at $0.006 or less before OCR. Other formats pay `document-converted` only.

Every charge comes after the file's record is saved, so a file that fails costs nothing, including any OCR already done on it. Only pages in the saved record are billed, and a page where OCR adds nothing new is not billed. The cost of a file in progress is held against your spending limit, so a run stops cleanly at the limit with every paid file saved. If the limit would run out on a long PDF, it is cut to the pages the limit covers, and the record says so.

### Sample output

```json
{
  "url": "<your-file-url>",
  "format": "pdf",
  "title": "Quarterly Solar Report",
  "pageCount": 2,
  "markdown": "# Quarterly Solar Report\n\nThe cooperative keeps a small network of rooftop solar sites and reports on them every quarter.\n\n| Region | Sites | Output kWh |\n| --- | --- | --- |\n| North | 12 | 48200 |\n| Central | 9 | 36150 |\n\n# Maintenance Notes\n\nInverters at two sites were replaced in the second month of the quarter.",
  "chunks": [
    {
      "index": 0,
      "page": 1,
      "text": "# Quarterly Solar Report\n\nThe cooperative keeps a small network of rooftop solar sites and reports on them every quarter.\n\n| Region | Sites | Output kWh |\n| --- | --- | --- |\n| North | 12 | 48200 |\n| Central | 9 | 36150 |"
    },
    {
      "index": 1,
      "page": 2,
      "text": "# Maintenance Notes\n\nInverters at two sites were replaced in the second month of the quarter."
    }
  ],
  "tables": [
    {
      "page": 1,
      "title": null,
      "headers": ["Region", "Sites", "Output kWh"],
      "rows": [
        { "Region": "North", "Sites": "12", "Output kWh": "48200" },
        { "Region": "Central", "Sites": "9", "Output kWh": "36150" }
      ]
    }
  ],
  "ocrPages": 0,
  "status": "converted",
  "warnings": []
}
```

### Integrations

To run it after any Actor that collects file links, choose that Actor's dataset in the Dataset field. A run can read only a dataset chosen in that field. It also works from n8n, Make, Zapier and AI agents, or through the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/VERRR~document-to-markdown/runs?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "urls": ["<your-file-url>"] }'
```

### Use cases

- Prepare PDF and Word documents for a RAG pipeline, with page numbers for citations.
- Convert PDF to Markdown for an LLM or AI agent.
- Extract tables from PDF, Word, Excel and HTML files as JSON rows.
- Read scanned PDFs with OCR.
- Turn the files found by a scraper into searchable text, in the same workflow.

### Limits and FAQ

- **How good is PDF table extraction?** It works from the position of the text, so it finds tables whose columns are clearly aligned. Anything less certain stays in the body text, because a wrong table is worse than none. That includes tables with long wrapped cells, and tables with only two columns and no numbers. A table that spans a page break comes out as one table per page. Headers that span several lines or columns are rebuilt as well as possible, so check them on complex tables. If no header can be found, columns are named `Column 1`, `Column 2` and so on. Footnote marks and exponents stay attached to their value as superscripts, such as `1979¹⁷` or `10²³`.
- **What about Word, Excel, PowerPoint and HTML tables?** These are read from the file's own structure. A first row of numbers counts as data, so those columns get `Column N` names. Each XLSX sheet gives one table, with the first well-filled row as its header, so two tables side by side on one sheet come out as one. Excel dates become ISO dates, times read like `12:00`, and formulas give their last saved value. HTML tables used only for page layout are read as text.
- **Which files fail?** Password-protected files, corrupt files, old `.doc`, `.xls` and `.ppt` files, images and archives, and UTF-16 text files with no byte order mark, which read as binary. Failures are listed in the `FAILED_ITEMS` key-value record and are not billed. If every file fails, the run fails.
- **Which pages go to OCR?** A PDF page goes to OCR when an image covers at least half of it and it holds under 200 characters of text, so a page with a real text layer or a title page with a small logo never goes. Text the file already holds is always kept. What OCR reads is kept and billed only if it adds real new text: at least 8 new, confidently read words beside any text the page already has, or at least 3 if they make up half of what it read. Fewer than 3 new words are left out with no warning, so a one- or two-word stamp on a scan can be lost. With 3 or more that fall short of this rule, the words are left out and the file reads `partial` with a warning. OCR takes about 11 seconds a page at the default 2 GB of memory, so a 100-page scan runs for about 20 minutes. A cover, a slide or a photo whose caption OCR only repeats is left as it is and not billed. If OCR cannot read a page with confidence, as with a photo or a badly faded scan, nothing from it is kept, nothing is billed and the status says `partial`. A page with a large image that does not qualify for OCR also says `partial`, because text in the image is not read.
- **Does OCR handle other languages?** The OCR model is English. Other languages in the Latin alphabet read poorly and other scripts not at all. A bad text layer added by earlier OCR is kept as it is.
- **Do right-to-left PDFs work?** Hebrew and Arabic lines are put in reading order. Tables in those languages are not tested.
- **What is left out?** Images and charts, speaker notes, headers and footers in Word, Word footnotes and comments, hidden Word text, and page numbers for Word files, which have no fixed pages. Hidden PowerPoint slides are converted like any other slide.
- **How large can a file be?** A dataset record holds at most 9 MB. A longer document keeps its first pages whole, in the Markdown, the chunks and the tables alike, and drops the rest; the status becomes `partial`, the warning says how many pages are included, and only those pages are billed. A file that is one very long page or sheet is cut at its end. An Office file that unpacks to more than 200 MB is refused as a possible zip bomb, and HTML nested over 1,000 levels deep is refused. Files convert one at a time, and a file that needs more memory than the run has normally fails alone, unbilled, while the run carries on. A deliberately crafted file can still stop the whole run; documents already saved are kept and nothing is misbilled. If the platform restarts a crashed run, files saved in the last minute can run and bill again.
- **Do I need a proxy?** No. Files are fetched directly, and links must be public.
- **Which links are refused?** Anything that is not http or https, links with a user name and password in them, and any host that resolves to a private or internal address, at every redirect.
- **What if a file format changes?** This Actor is monitored weekly and repaired automatically.

### Legal note

This Actor fetches only the public links you give it and keeps no copy: files are held in memory while they are converted and the results go to your own dataset. You are responsible for your right to use each file, and for complying with applicable laws and the terms of the site that hosts it.

# Actor input Schema

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

Public http or https links to PDF, DOCX, PPTX, XLSX, HTML or plain text files. The format is detected from the file itself. An empty run converts a small sample.

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

A dataset of file links, for example the output of a scraper. To run after another Actor, pick that Actor's dataset here: this run can read only a dataset chosen in this field.

## `urlField` (type: `string`):

The dataset field that holds the file link. A dotted path reaches nested fields, and a field may hold a list of links. Leave empty to detect common names, with file fields such as pdfUrl and fileUrl read before url and link.

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

Read scanned PDF pages with optical character recognition (English). A page goes to OCR when an image covers at least half of it and it holds under 200 characters of text. Only pages where OCR adds new text are billed, as ocr-page events.

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

Only the first pages of a longer file are converted: PDF pages, PPTX slides or XLSX sheets. The record's status then says partial. Past 100 PDF pages, each further block of 100 pages bills one extra-pages event.

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

Larger files are skipped and not billed.

## `maxItems` (type: `integer`):

The most files one run converts, counting links from the list and the dataset together.

## Actor input object example

```json
{
  "urls": [],
  "urlField": "",
  "ocr": true,
  "maxPages": 200,
  "maxFileSizeMb": 50,
  "maxItems": 1000
}
```

# Actor output Schema

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

One record per file: url, format, title, pageCount, markdown, chunks, tables, ocrPages, status, warnings.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("verrr/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 = {}

# Run the Actor and wait for it to finish
run = client.actor("verrr/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 '{}' |
apify call verrr/document-to-markdown --silent --output-dataset

```

## MCP server setup

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