# PDF to Markdown (`jcwilk/pdf-to-markdown`) Actor

PDF to Markdown converter API for RAG and LLMs: clean Markdown, plain text or JSON chunks with page numbers. Tables, multi-column layouts, OCR for scanned pages. $0.002 per PDF + $0.00025 per page; failures are free.

- **URL**: https://apify.com/jcwilk/pdf-to-markdown.md
- **Developed by:** [John Wilkinson](https://apify.com/jcwilk) (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 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 API for RAG & LLMs (OCR, Tables)

**PDF to Markdown, text or RAG chunks in one API call.** $0.001 per document + $0.00012 per page (a 10-page PDF costs $0.0022), and scanned pages with OCR cost $0.001. Failed or empty files are free. No GPU, no LLM, no per-month fee.

Convert PDFs to **clean Markdown, plain text, or RAG-ready JSON chunks with page numbers**, built for LLM, RAG, vector database, and AI agent pipelines. Give it PDF URLs or upload a file and get one dataset item per document back. Each item has the Markdown, title, page count, language, optional chunks, and a status/error field. **OCR for scanned PDFs**, **tables as Markdown tables**, **two-column layouts in reading order**, and **repeated headers, footers, and page numbers removed**.

**Pay only for what you convert:** a small fee per document plus a fee per page. Failed downloads, non-PDFs, and empty documents are free.

### What it does

- **PDF → Markdown**: headings (`#`, `##`, … detected from font size and weight), paragraphs re-flowed across line breaks, de-hyphenation, bullet and numbered lists.
- **Tables → Markdown tables** (ruled tables, including rows stacked without horizontal lines). Choose `markdown`, `text` (tab-separated), or `skip`.
- **Multi-column layouts** (scientific papers, reports): left column, then right column, with full-width titles and abstracts kept in place.
- **OCR for scanned pages** (Tesseract 5): `auto` OCRs only the pages that have no text layer, so mixed documents stay cheap. Languages: eng, deu, fra, spa, ita, por, nld, or combinations like `eng+deu`.
- **RAG chunking**: heading-aware chunks with `chunkSize` and `chunkOverlap` (in approximate tokens). Each chunk has `pageStart`, `pageEnd`, and its `headings` path (e.g. `["3 Model Architecture", "3.2 Attention"]`) for citations and filtering.
- **Page range** (`1-5,8,10-`): convert only what you need and pay only for those pages.
- **Metadata**: title (from PDF metadata or the first heading), author, total and processed pages, OCR page count, detected language, raw PDF info.
- **Clean errors**: each document gets `status` = `success` / `empty` / `error` and a readable `error` (404, "Not a PDF (got HTML page)", password-protected, too large…). One bad URL never fails the run.
- **Fast and light**: about 0.1 s per text page on one CPU core; the default is 1 GB of memory. No GPU or ML models.

### Input

| Field | Default | Description |
|---|---|---|
| `urls` | – | List of PDF URLs (`[{ "url": "https://…/file.pdf" }]` or plain strings) |
| `file` | – | Upload a PDF from your computer (Console) |
| `base64Files` | – | API/agents: `[{ "name": "a.pdf", "data": "<base64>" }]` |
| `outputFormat` | `markdown` | `markdown`, `text`, or `json` (Markdown + chunks + per-page Markdown) |
| `chunking` | `false` | Add RAG chunks (always on for `json`) |
| `chunkSize` / `chunkOverlap` | `512` / `64` | Approximate tokens (1 token ≈ 4 chars) |
| `ocr` | `auto` | `auto` (scanned pages only), `off`, `force` |
| `ocrLanguage` | `eng` | Tesseract code(s), e.g. `deu`, `eng+fra` |
| `pageRange` | all | e.g. `1-10`, `2,4,6-` |
| `tables` | `markdown` | `markdown`, `text`, `skip` |
| `imageCaptions` | `false` | Insert `![Figure 1: …](page-3-image-1)` placeholders with nearby caption text |
| `removeHeadersFooters` | `true` | Drop running headers, footers, and page numbers |
| `maxPagesPerDocument` | `1000` | Safety cap |
| `maxFileSizeMb` | `100` | Download limit |
| `maxConcurrency` | `2` | Parallel documents |

#### Example input

```json
{
  "urls": [
    { "url": "https://arxiv.org/pdf/1706.03762" },
    { "url": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf" }
  ],
  "outputFormat": "json",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "ocr": "auto"
}
```

### Output

One dataset item per PDF (abridged):

```json
{
  "source": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf",
  "status": "success",
  "error": null,
  "title": "The NIST Cybersecurity Framework (CSF) 2.0",
  "language": "en",
  "author": null,
  "totalPages": 32,
  "processedPages": 32,
  "ocrPages": 0,
  "charCount": 66741,
  "markdown": "# The NIST Cybersecurity Framework (CSF) 2.0\n\n## National Institute of Standards and Technology\n\n…\n\n### Abstract\n\nThe NIST Cybersecurity Framework (CSF) 2.0 provides guidance to industry, government agencies, and other organizations to manage cybersecurity risks…",
  "chunks": [
    {
      "chunkIndex": 3,
      "text": "### Preface\n\nThe Cybersecurity Framework (CSF) 2.0 is designed to help organizations of all sizes and sectors…",
      "pageStart": 4,
      "pageEnd": 5,
      "headings": ["The NIST Cybersecurity Framework (CSF) 2.0", "National Institute of Standards and Technology", "Preface"],
      "charCount": 1901,
      "approxTokens": 475
    }
  ],
  "pageTexts": [{ "page": 1, "ocr": false, "markdown": "# The NIST Cybersecurity Framework (CSF) 2.0 …" }],
  "warnings": [],
  "elapsedSec": 3.26
}
```

A table from a government report comes out as:

```markdown
| Notice Date | Effective | Received | Company | City | No. Of | Layoff/Closure |
|---|---|---|---|---|---|---|
| 06/22/2015 | 03/25/2016 | 07/01/2015 | Maxim Integrated Product | San Jose | 150 | Closure Permanent |
```

Download results as JSON, JSONL, CSV, or Excel, or read them through the Apify API.

### Use it from code, LangChain, LlamaIndex, or an AI agent

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("jcwilk/pdf-to-markdown").call(run_input={
    "urls": ["https://arxiv.org/pdf/1706.03762"], "outputFormat": "json", "chunkSize": 400})
for doc in client.dataset(run["defaultDatasetId"]).iterate_items():
    for ch in doc.get("chunks", []):
        embed_and_store(ch["text"], metadata={"source": doc["source"], "page": ch["pageStart"], "section": ch["headings"]})
```

The Actor also works with the **Apify MCP server**, so Claude, Cursor, and other MCP clients can call it as a "read this PDF" tool.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start (charged by Apify, per GB of run memory) | $0.0005 per GB |
| Document converted | $0.001 |
| Page converted (text layer) | $0.00012 ($0.12 per 1,000 pages) |
| Page converted with OCR | $0.001 ($1 per 1,000 pages) |

Examples (excluding the start fee): 10-page report **$0.0022**, 100-page manual **$0.013**, 20-page scanned contract **$0.021**. Batch many PDFs per run to spread the start fee. Failed, empty, and non-PDF inputs are not charged. Use `pageRange` to pay for fewer pages.

### FAQ

**How is this different from LLM-based PDF parsers?** It uses deterministic open-source parsing (pdfplumber, pypdfium2, Tesseract), so it's cheap, fast and repeatable. Very complex layouts (math formulas, charts) are not reconstructed the way vision-LLM parsers try to.

**Does it work on scanned PDFs?** Yes. With `ocr: "auto"` (the default), pages without a text layer go through Tesseract OCR. Set the language with `ocrLanguage`. With `force`, every page is OCR'd (useful when the text layer is broken).

**How good are the tables?** Ruled and grid tables (forms, financial and government reports) come out as clean Markdown tables. Tables drawn only with whitespace, such as "booktabs" tables in academic papers, are kept as text in reading order rather than turned into tables.

**Can it handle two-column papers?** Yes. Columns are read left, then right, and full-width titles and abstracts stay at the top.

**What chunk size should I use?** 300–800 tokens with 10–15% overlap suits most embedding models. Chunks never split mid-sentence unless one sentence is longer than the chunk.

**Is my document stored?** Documents are processed in memory during the run. Only the results you see in the dataset are saved, under your Apify storage retention settings.

**Password-protected PDFs?** They return `status: "error"` with a clear message and are not charged.

**Does it support DOCX or images?** Not yet. It handles PDFs only, which keeps it fast and cheap.

**How fast is it?** About 0.1–0.2 s per text page and 2–3 s per OCR page on one CPU core. For large OCR jobs, raise the memory to 2–4 GB to get more CPU.

### Limitations

- Math formulas come out as linear text, not LaTeX.
- Whitespace-only tables are not converted into Markdown tables.
- Image captions are placeholders that use the caption text printed in the PDF. No AI image description.
- Layout detection handles one or two columns. Three or more columns may interleave.

### Run locally (no Apify account needed)

```bash
python -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt          # plus: apt install tesseract-ocr
mkdir -p storage/key_value_stores/default
echo '{"urls":["https://arxiv.org/pdf/1706.03762"]}' > storage/key_value_stores/default/INPUT.json
python -m src                                # results in storage/datasets/default/
pytest -q                                    # unit and end-to-end tests
python scripts/smoke.py                      # smoke test on tests/smoke/pdfs (see scripts/fetch_smoke_pdfs.sh)
```

Built on open-source libraries with permissive licenses: pdfplumber/pdfminer.six (MIT), pypdfium2 (Apache-2.0/BSD-3), Tesseract and pytesseract (Apache-2.0), py3langid (BSD-3). No AGPL or GPL dependencies.

# Actor input Schema

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

Direct links to PDF files. Redirects are followed.

## `file` (type: `string`):

Upload a PDF from your computer (stored in the run's key-value store).

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

For API/agent callers: list of {"name": "a.pdf", "data": "<base64>"} objects or base64 strings.

## `outputFormat` (type: `string`):

markdown = Markdown with headings, lists and tables. text = plain text. json = Markdown plus RAG chunks and per-page Markdown with page metadata.

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

Add heading-aware chunks (with page range and heading path) ready for embedding. Always on for JSON output.

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

Target chunk size in tokens (1 token ≈ 4 characters).

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

Trailing paragraphs repeated at the start of the next chunk, up to this many tokens.

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

auto = OCR only pages with no text layer (recommended). off = never OCR (fastest, cheapest). force = OCR every page.

## `ocrLanguage` (type: `string`):

Tesseract language code(s), e.g. eng, deu, fra, spa, ita, por, nld, or combined: eng+deu.

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

Pages to convert, e.g. 1-5,8,10-. Empty = all pages. You are only charged for converted pages.

## `tables` (type: `string`):

markdown = convert detected tables to Markdown tables. text = tab-separated rows. skip = don't detect tables (faster).

## `imageCaptions` (type: `boolean`):

Insert ![caption](page-N-image-M) placeholders where images appear, using the nearby 'Figure ...' caption text. Off by default.

## `removeHeadersFooters` (type: `boolean`):

Drop running headers, footers and page numbers repeated on most pages.

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

Safety cap on pages converted per PDF.

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

Max file size (MB).

## `maxConcurrency` (type: `integer`):

Max parallel documents.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://arxiv.org/pdf/1706.03762"
    }
  ],
  "outputFormat": "markdown",
  "chunking": false,
  "chunkSize": 512,
  "chunkOverlap": 64,
  "ocr": "auto",
  "ocrLanguage": "eng",
  "tables": "markdown",
  "imageCaptions": false,
  "removeHeadersFooters": true,
  "maxPagesPerDocument": 1000,
  "maxFileSizeMb": 100,
  "maxConcurrency": 2
}
```

# Actor output Schema

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

One row per PDF with Markdown/text, metadata and chunks.

# 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 = {
    "urls": [
        {
            "url": "https://arxiv.org/pdf/1706.03762"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jcwilk/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 = { "urls": [{ "url": "https://arxiv.org/pdf/1706.03762" }] }

# Run the Actor and wait for it to finish
run = client.actor("jcwilk/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 '{
  "urls": [
    {
      "url": "https://arxiv.org/pdf/1706.03762"
    }
  ]
}' |
apify call jcwilk/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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