# PDF to Markdown with OCR for RAG and LLMs (`doodlebyte/pdf-to-markdown`) Actor

Convert PDFs, including scanned ones, to clean Markdown for RAG and LLMs. Keeps headings, lists, tables and two-column reading order, reads scanned pages with OCR, and returns token-sized chunks with heading paths and page numbers.

- **URL**: https://apify.com/doodlebyte/pdf-to-markdown.md
- **Developed by:** [Aditya](https://apify.com/doodlebyte) (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 $0.80 / 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.
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

## PDF to Markdown with OCR for RAG and LLMs

Convert PDFs into clean Markdown and RAG-ready chunks. This includes
scanned PDFs, which are read with OCR.

- **Scanned pages are read, not refused.** Pages that are images without
  a text layer go through OCR (Tesseract):
  - JBIG2 and JPEG 2000 scans are supported;
  - text pages use their exact text layer instead;
  - mixed PDFs get both, page by page.
- **Structure kept:**
  - headings are detected from font sizes (`#`, `##`, `###`);
  - bulleted and numbered lists become Markdown lists, including bullets
    drawn as graphics;
  - words hyphenated across a line break are joined;
  - superscript footnote marks stay on their line.
- **Tables become Markdown tables.**
  - Columns come from the layout, so right-aligned numbers, empty cells
    and headers spanning several columns are handled.
  - A table that continues on the next page stays one table.
  - In chunks, large tables are split between rows, with the header row
    repeated in every piece.
- **Code listings become fenced code blocks** (monospace text).
- **Two-column layouts are read column by column.** Academic papers and
  brochures come out in reading order, not interleaved line by line.
- **Chunks for retrieval:**
  - each chunk stays within one section and never crosses a heading;
  - each carries its **heading path** (for example `Report > Shipping`), its
    **page range** and its **token count** (GPT tokenizer, configurable
    maximum).
- **Upload or link:** pass public URLs, or upload PDFs from your computer.
- **Page ranges:** convert only `1-5,8` if that is all you need.

### Use cases

- **RAG and LLM ingestion:** turn PDFs into chunks that keep their
  section context, ready for embedding in a vector database.
- **Scanned archives:** make old scanned reports, letters and manuals
  searchable text.
- **Research papers:** convert two-column papers into Markdown in
  reading order, with their tables.
- **Reports and statements:** extract tables of figures as Markdown
  tables.

### Pricing

Pay per event. Platform usage is included. Higher Apify discount tiers pay
less: down to $0.0008 per text page and $0.0035 per OCR page (see the
Pricing tab).

| Event | Price |
|---|---|
| Page converted from its text layer | $0.001 |
| Page read by OCR (scanned) | $0.005 |
| Blank pages, failed downloads, password-protected or invalid files, files with no text | free |

A 20-page text PDF costs $0.02. A 20-page scan costs $0.10. Your spending
limit is respected: OCR stops when the limit is reached, and remaining
pages are listed in `scannedPagesNotRead`.

### Input

| Field | Meaning |
|---|---|
| `pdfUrls` | Public PDF URLs, up to 50 MB each |
| `pdfFiles` | PDFs uploaded from your computer |
| `pages` | Optional page selection, e.g. `1-5,8` |
| `ocr` | `auto` (scanned pages only, default), `always` (every page; for garbled text layers) or `off` |
| `ocrLanguages` | Languages of scanned text, e.g. `["eng", "deu"]` (default English; 29 available) |
| `chunking` | Return RAG chunks (default `true`) |
| `chunkMaxTokens` | Maximum tokens per chunk, 100–4000 (default 500) |

### Output

One dataset item per PDF:

```json
{
  "url": "https://example.com/report.pdf",
  "title": "Quarterly Operations Report",
  "pageCount": 12,
  "pagesConverted": 12,
  "textPages": 10,
  "ocrPages": 2,
  "ocrConfidence": 91,
  "markdownUrl": "https://api.apify.com/v2/key-value-stores/…/records/document-1.md",
  "markdown": "# Quarterly Operations Report\n\n…",
  "chunks": [
    { "headings": ["Quarterly Operations Report", "Shipping performance"], "pageFrom": 1, "pageTo": 2, "tokens": 412, "text": "…" }
  ]
}
```

`ocrConfidence` is Tesseract's mean confidence (0–100) over the OCR pages.
Below about 70, check the scan quality.

### FAQ

**Does it work on scanned PDFs?** Yes. Pages without a text layer are read
with OCR automatically (`ocr: auto`). You pay the OCR price only for those
pages.

**Which languages does OCR support?** 29, including English, German,
French, Spanish, Portuguese, Italian, Dutch, Polish, Russian, Arabic,
Hindi, Chinese, Japanese and Korean. Pick them in `ocrLanguages`. Pages
with a text layer need no language setting.

**How do I use the output in a RAG pipeline?** Take `chunks` from each
dataset item. Each chunk's `text` is ready to embed. Store `headings`,
`pageFrom` and `pageTo` as metadata, to cite sources.

**Can I call it from code?** Yes, through the Apify API or the Apify
clients for JavaScript and Python (see the API tab), or from AI agents
through Apify's MCP server.

**What does it cost for a whole document?** Pages × price: a 50-page text
PDF costs $0.05. Set a maximum cost per run in the run options to cap
spending.

### Speed

- Text pages take milliseconds.
- OCR takes a few seconds per page and grows with the amount of text. A
  dense letter-size page took about 8 seconds at the default 2 GB in
  tests on Apify.
- More memory gives more CPU (one core per 4 GB): use 4 GB for large
  scanned documents.

### Limits

- **Tables:**
  - a cell that wraps onto a second line becomes its own row;
  - the first row is always the header;
  - tables in scans work when their columns are clearly spaced.
- **OCR:**
  - reads upright pages; sideways pages are not rotated;
  - handwriting is not supported;
  - bold section headings in a scan that are no larger than the body text
    stay in the paragraph.
- **Layouts:**
  - three or more columns are read as one;
  - right-to-left scripts (Arabic, Hebrew) may come out with words in
    reverse order.
- **Vector text:** text drawn as vector shapes (no text layer and no
  image) is found only with `ocr: always`.
- **Support:** report problems in the Issues tab. Fixes ship as new Actor
  versions.

Built with [pdf.js](https://github.com/mozilla/pdf.js) (Apache-2.0),
[tesseract.js](https://github.com/naptha/tesseract.js) (Apache-2.0) with
Tesseract's trained data (Apache-2.0), and
[gpt-tokenizer](https://github.com/niieani/gpt-tokenizer) (MIT).

# Actor input Schema

## `pdfUrls` (type: `array`):

Public URLs of PDF files, up to 50 MB each. Text PDFs and scanned PDFs both work.

## `pdfFiles` (type: `array`):

Or upload PDF files from your computer (stored in your Apify key-value store).

## `pages` (type: `string`):

Optional page selection, e.g. "1-5,8". Empty means all pages.

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

Auto: read scanned pages (images without a text layer) by OCR and use the text layer everywhere else. Always: read every page by OCR, for PDFs whose text layer is garbled. Off: never use OCR.

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

Languages of the scanned text. Pick all that appear; each extra language slows OCR down.

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

Also return chunks that never cross a heading, each with its heading path, page range and token count.

## `chunkMaxTokens` (type: `integer`):

Counted with a GPT tokenizer (100 to 4000).

## Actor input object example

```json
{
  "pdfUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "chunking": true,
  "chunkMaxTokens": 500
}
```

# Actor output Schema

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

Default dataset: one item per PDF with its URL, title, page counts (text and OCR), OCR confidence, the Markdown (up to 100,000 characters), RAG chunks when enabled, a link to the full Markdown, and an error message when a PDF could not be converted.

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

Default key-value store: the full Markdown of each converted PDF, as records document-1.md, document-2.md and so on.

# 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 = {
    "pdfUrls": [
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("doodlebyte/pdf-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 = { "pdfUrls": [{ "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("doodlebyte/pdf-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 '{
  "pdfUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ]
}' |
apify call doodlebyte/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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