# PDF & Document to Markdown — RAG Chunks, Tables, OCR (`frayess_mosbehi/doc-to-markdown-rag`) Actor

Convert PDF, DOCX and PPTX to clean Markdown, JSON and RAG-ready chunks with tables and automatic OCR. $1 per 1,000 pages, no start or per-file fee.

- **URL**: https://apify.com/frayess_mosbehi/doc-to-markdown-rag.md
- **Developed by:** [firas_mosbehi](https://apify.com/frayess_mosbehi) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page 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 — RAG Chunks, Tables, OCR

Convert PDF, DOCX and PPTX (plus XLSX and HTML) to clean Markdown, JSON and ready-to-embed RAG chunks — with real table extraction and automatic OCR only where a page needs it.

### 💲 Pricing

**$1 per 1,000 pages, $4 per 1,000 OCR pages, no start fee, no per-file fee.**

- A page is charged only after it has been converted successfully.
- OCR pricing applies only to pages that have no text layer (scans, photos) — text pages in the same file stay at the normal price.
- The optional *Accurate* layout engine (AI layout model, slower) is billed at the OCR page price for every page.
- Never charged: failed downloads, encrypted or corrupted files, pages outside your `pageRange`, pages that produced no text.
- Set a **maximum cost per run** in Apify and the Actor stops before exceeding it — it never converts pages it can't charge for.

How pages are counted: PDF = pages, PPTX = slides, XLSX = sheets, DOCX / HTML = one page per 3,000 characters of output.

### 📄 Output example

Markdown (`markdown` field of the document item):

```markdown
### Quarterly Sales

Units sold per region.

| Region   | Manager   |   Q1 units |   Q2 units |
|----------|-----------|------------|------------|
| Region 1 | Manager 1 |        101 |        112 |
```

One chunk item (one dataset item per chunk):

```json
{
  "type": "chunk",
  "sourceUrl": "https://example.com/report.pdf",
  "chunkIndex": 4,
  "text": "Remote work is allowed up to three days per week...",
  "headingPath": ["Employee Handbook", "Working Hours", "Remote Work"],
  "pageStart": 3,
  "pageEnd": 4,
  "tokenEstimate": 412
}
```

Each document item also contains `title`, `language`, `pageCount`, `pagesProcessed`, `ocrPages`, `tables[]` (each with `page`, `markdown` and `rows`), `warnings[]` and `processingMs`. Very large Markdown (over ~5 MB) is saved to the run's key-value store and linked as `markdownUrl`.

### 🎯 Use cases

- **RAG / vector DB ingestion** — chunks carry their heading path and page span; load them straight into Pinecone, Qdrant, Weaviate, pgvector or Chroma.
- **AI agents** — give an agent a clean Markdown view of any document.
- **Search indexing** — index text and tables with page numbers for citations.
- **Document migration** — move PDFs and Office files into wikis, CMSs or Git as Markdown.

### 🚀 How to use

1. **URLs** — paste public or signed links (S3, Google Cloud Storage, Dropbox direct links…) into *Document URLs*.
2. **Files on Apify** — upload files to a key-value store and list them in *Files on Apify* as `{"storeId": "...", "key": "report.pdf"}`.
3. Run. Each document becomes one `document` item; each chunk becomes one `chunk` item. Use the **Documents** and **RAG chunks** views in the Output tab.

#### API call

```bash
curl -X POST "https://api.apify.com/v2/acts/frayess_mosbehi~doc-to-markdown-rag/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": [{"url": "https://example.com/report.pdf"}], "chunkSize": 800}'
```

#### Python / LangChain

```python
from apify_client import ApifyClient
from langchain_core.documents import Document

run = ApifyClient("<APIFY_TOKEN>").actor("frayess_mosbehi/doc-to-markdown-rag").call(
    run_input={"documentUrls": [{"url": "https://example.com/report.pdf"}]}
)
items = ApifyClient("<APIFY_TOKEN>").dataset(run["defaultDatasetId"]).iterate_items()
docs = [
    Document(page_content=i["text"], metadata={"source": i["sourceUrl"], "headings": i["headingPath"], "page": i["pageStart"]})
    for i in items if i["type"] == "chunk"
]
```

#### n8n / Make / Zapier

Use the official Apify app: action **Run Actor** with `frayess_mosbehi/doc-to-markdown-rag`, then **Get dataset items** and filter on `type = chunk`.

### 🔒 Privacy & security

- Files are processed in memory or temporary storage only; nothing is kept after the run except your own results in your own Apify storage.
- Document text is never logged — logs contain only file names, page counts and timings.
- No third-party APIs: conversion and OCR run entirely inside the Actor container.
- Downloads are protected: private/internal network addresses are refused (also after redirects), file type is checked from the file content, size is capped, and Office files are checked against zip bombs.

### ❓ FAQ

**Which formats are supported?** PDF, DOCX, PPTX, XLSX and HTML. Legacy `.doc`/`.ppt`/`.xls` are not supported — save them as the modern format first.

**What are the limits?** Default 100 MB per file and 500 pages per document (both configurable), and 10 minutes per document.

**Fast or accurate layout?** *Fast* (default) reads text-layer pages directly and OCRs scanned pages with Tesseract — it handles typical reports, papers and manuals, including two-column pages and ruled tables. Tables without borders stay as plain text, and tables inside scans are not detected. *Accurate* runs an AI layout model on every page and handles complex layouts and scanned tables, but is much slower and billed at the OCR page price.

**How is OCR detected?** With `ocr: auto` (default), each PDF page is checked for a text layer. Only pages without one are OCR'd and charged at the OCR price. Use `always` for PDFs whose text layer is broken, or `never` to skip scans entirely (those pages are reported in `warnings` and not charged).

**Which OCR languages?** English, French, German, Spanish, Italian, Portuguese and Dutch (`eng`, `fra`, `deu`, `spa`, `ita`, `por`, `nld`).

**How is billing counted?** One `page-converted` event per converted text page, or one `ocr-page-converted` event per OCR page — never both for the same page. Chunks, tables and JSON output are included at no extra cost.

**What happens with an encrypted or broken file?** You get a document item with `error` set and nothing is charged.

# Changelog

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

# Actor input Schema

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

Public or signed URLs of your PDF, DOCX, PPTX, XLSX or HTML files.

## `keyValueStoreRecords` (type: `array`):

Files already stored on Apify, as objects {"storeId": "...", "key": "..."}. Leave storeId empty for this run's default store.

## `outputFormats` (type: `array`):

Which full-document outputs to include in each document item.

## `chunking` (type: `boolean`):

Also output heading-aware chunks (one dataset item per chunk) ready for embedding.

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

Approximate maximum tokens per chunk (estimated at ~4 characters per token), including overlap.

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

Approximate tokens repeated from the end of the previous chunk.

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

auto = OCR only pages without a text layer; always = OCR every page; never = no OCR.

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

Tesseract language codes used for OCR, e.g. eng, fra, deu, spa, ita, por, nld.

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

Detect table structure and output tables as Markdown and as rows in tables\[].

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

Only convert these PDF pages, e.g. "1-20", "5" or "10-". Empty = all pages.

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

Safety cap: pages beyond this are skipped (with a warning) and not charged.

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

Files larger than this are rejected without charge.

## `layout` (type: `string`):

fast = no AI layout model: PDF pages with a text layer are read directly (font-based headings, ruled tables, columns) and scanned pages are OCR'd with Tesseract (no table detection on scans). accurate = AI layout model on every page: better on complex layouts and tables in scans, ~100x slower, and every page is billed at the OCR page price.

## Actor input object example

```json
{
  "documentUrls": [
    {
      "url": "https://raw.githubusercontent.com/firas-apify-actor/doc-to-markdown-samples/main/sample.pdf"
    }
  ],
  "keyValueStoreRecords": [],
  "outputFormats": [
    "markdown"
  ],
  "chunking": true,
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "ocr": "auto",
  "ocrLanguages": [
    "eng",
    "fra"
  ],
  "extractTables": true,
  "pageRange": "",
  "maxPagesPerDocument": 500,
  "maxFileSizeMb": 100,
  "layout": "fast"
}
```

# Actor output Schema

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

One item per input document: Markdown (or markdownUrl), tables, page counts, OCR pages, warnings and errors.

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

Heading-aware text chunks with headingPath, page span and token estimate, ready for embedding.

## `largeFiles` (type: `string`):

Markdown, text or JSON larger than ~5 MB, stored as <docId>.md / .txt / .json.

# 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": [
        {
            "url": "https://raw.githubusercontent.com/firas-apify-actor/doc-to-markdown-samples/main/sample.pdf"
        }
    ],
    "keyValueStoreRecords": [],
    "outputFormats": [
        "markdown"
    ],
    "chunkSize": 1000,
    "chunkOverlap": 100,
    "ocrLanguages": [
        "eng",
        "fra"
    ],
    "pageRange": "",
    "maxPagesPerDocument": 500,
    "maxFileSizeMb": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("frayess_mosbehi/doc-to-markdown-rag").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": [{ "url": "https://raw.githubusercontent.com/firas-apify-actor/doc-to-markdown-samples/main/sample.pdf" }],
    "keyValueStoreRecords": [],
    "outputFormats": ["markdown"],
    "chunkSize": 1000,
    "chunkOverlap": 100,
    "ocrLanguages": [
        "eng",
        "fra",
    ],
    "pageRange": "",
    "maxPagesPerDocument": 500,
    "maxFileSizeMb": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("frayess_mosbehi/doc-to-markdown-rag").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": [
    {
      "url": "https://raw.githubusercontent.com/firas-apify-actor/doc-to-markdown-samples/main/sample.pdf"
    }
  ],
  "keyValueStoreRecords": [],
  "outputFormats": [
    "markdown"
  ],
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "ocrLanguages": [
    "eng",
    "fra"
  ],
  "pageRange": "",
  "maxPagesPerDocument": 500,
  "maxFileSizeMb": 100
}' |
apify call frayess_mosbehi/doc-to-markdown-rag --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,frayess_mosbehi/doc-to-markdown-rag"
        }
    }
}
```

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/NDC8MHtGLyhOwlT9E/builds/EMycuZBv1BtC6l6sT/openapi.json
