# PDF to Markdown with Page Numbers – Tables, RAG Chunks (`ryumoon-data/pdf-to-markdown`) Actor

Convert PDF URLs to clean Markdown with page numbers for citations. Headings, lists, real Markdown tables, 2-column reading order, metadata and RAG chunks that keep their page range. Scanned pages are flagged "needs OCR" and never charged. $0.50 per 1,000 pages.

- **URL**: https://apify.com/ryumoon-data/pdf-to-markdown.md
- **Developed by:** [Ryu Moon](https://apify.com/ryumoon-data) (community)
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 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 to Markdown with Page Numbers – Tables, RAG Chunks

Turn PDF links into clean Markdown that an LLM, a RAG pipeline or Obsidian can use straight away, and that you can **cite back to the exact page**.

- 📄 **Page numbers everywhere.** Every page starts with `<!-- page 3 -->` in the Markdown, per-page rows carry `page`, and every RAG chunk carries `pageStart`, `pageEnd` and a ready-made `citation` ("Annual Report 2024, pp. 12-13").
- 📊 **Real Markdown tables.** Ruled tables and aligned number columns (financial statements, statistics tables) come out as `| a | b |` tables, not jumbled lines.
- 📰 **Two-column papers read in the right order.** Left column first, then right, with the title block and full-width figures kept in place.
- 🧱 **Structure kept.** Headings from font size and numbering (`#`, `##`, `###`), bullet and numbered lists, paragraphs re-joined across line breaks, hyphenated words repaired, running headers, footers and page numbers removed.
- 🧩 **RAG chunks built in.** Choose chunk size and overlap; chunks break at headings, paragraphs and sentences, tables are split by rows with the header repeated.
- 🔍 **Honest about scans.** Image-only (scanned) pages are flagged `needs_ocr` with their page numbers. We never invent text, and those pages are **free**.
- 💸 **Pay per page converted, nothing else.** No start fee, no per-PDF fee. Failed downloads, scanned pages and blank pages cost nothing.

#### What you get

| Output format | One row per | Key fields |
|---|---|---|
| `document` (default) | PDF | `markdown` (with page markers), `metadata` (title, author, subject, keywords, creator, producer, dates), `pageCount`, `textPages`, `needsOcrPages`, `tables`, `markdownFileUrl` (.md download), optional `pages` array and plain `text` |
| `pages` | page | `page`, `status` (`text` / `needs_ocr` / `empty` / `unreadable_text`), `markdown`, `headings`, `tables`, `citation` |
| `chunks` | chunk | `text`, `pageStart`, `pageEnd`, `pages`, `headings` (section path), `tokenEstimate`, `citation`, `chunkIndex` |

Example chunk (from a real run on the BERT paper, shortened):

```json
{
  "citation": "BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding, p. 1",
  "pageStart": 1, "pageEnd": 1,
  "headings": ["BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding", "1 Introduction"],
  "tokenEstimate": 351,
  "text": "## 1 Introduction\n\nLanguage model pre-training has been shown to be effective for improving many natural language processing tasks ..."
}
```

Example table (from a real run on Apple's 2023 Form 10-K, page 31, shortened):

```markdown
<!-- page 31 -->
### CONSOLIDATED STATEMENTS OF OPERATIONS
|  | 2023 | 2022 | 2021 |
|---|---|---|---|
| Net sales: |  |  |  |
| Products | $298,085 | $316,199 | $297,392 |
| Services | 85,200 | 78,129 | 68,425 |
| Total net sales | 383,285 | 394,328 | 365,817 |
```

#### How to use it

1. Paste direct PDF links into **PDF URLs** (arXiv `/abs/` pages, Google Drive file links and Dropbox share links are converted automatically).
2. Pick an **Output format**: whole document, one row per page, or RAG chunks.
3. Optional: a **Page range** like `1-5, 8, 12-` to convert (and pay for) only those pages.
4. Run, then download JSON, CSV or Excel, or call it from the API, Make, Zapier, n8n, LangChain or LlamaIndex.

#### Pricing

**$0.50 per 1,000 pages converted** (`page` event, $0.0005 each). A 15-page paper costs $0.0075, a 100-page report $0.05. No start fee and no per-PDF fee. Pages flagged `needs_ocr`, blank pages, pages outside your page range and PDFs that fail to download or open are never charged. Set a maximum cost per run in Apify and the Actor stops cleanly when it is reached; **Max pages per PDF** (default 20) also caps what one document can cost.

#### Limits (please read)

- **No OCR.** Scanned or image-only pages are reported as `needs_ocr` with their page numbers and are not charged. Run those through an OCR tool.
- **Layout is detected heuristically** (PDFs carry no real structure): headings come from font sizes and numbering, tables from ruling lines and aligned columns. Dense multi-level tables, maths, chart labels and very designed layouts can come out flatter. Pages with three or more columns are read as one column. Rotated or upside-down text (e.g. labels in figures) is skipped and counted in `rotatedCharsSkipped`.
- **Size and time:** 100 MB per file and 20 pages per PDF by default (raise up to 500 MB / 5,000 pages), 300 s per PDF. 1 GB handled every test PDF (largest peak about 420 MB); keep **Parallel PDFs** at 1 on 1 GB.
- **Links must point at the PDF file itself**, not a web page that shows it. You get a clear `NOT_A_PDF` row if not.
- Use it on documents you're allowed to process: public or your own business documents. It doesn't bypass logins, paywalls or DRM. Password-protected PDFs work only if you supply the password.

#### Errors are rows, not crashes

Every PDF that can't be converted still gets a row with `status: "failed"`, an `errorCode` (`HTTP_404`, `NOT_A_PDF`, `TOO_LARGE`, `ENCRYPTED`, `TIMEOUT`, `PARSE_ERROR`, `BAD_PAGE_RANGE`, `OUT_OF_MEMORY`) and a plain-English `error`. Fully scanned PDFs get `status: "needs_ocr"`.

#### FAQ

**Why page markers as HTML comments?** They're invisible when the Markdown is rendered but easy to find with code or by an LLM, so answers can quote "page 12".

**How big are chunks?** `chunkSize` is in characters (about 4 characters per token, so the default 1,500 is roughly 375 tokens). Overlap repeats whole sentences from the end of the previous chunk, never across a new section heading.

**Can it read tables from annual reports?** Yes for most statement-style tables (labels on the left, numbers in columns) and any ruled table. Very wide multi-level tables may merge some header cells.

#### Related tools by Ryu Moon

- [Website to Markdown for AI, LLM & RAG](https://apify.com/ryumoon-data/website-to-markdown-rag): the same clean Markdown for web pages.
- [RSS Feed Converter](https://apify.com/ryumoon-data/rss-feed-converter): RSS, Atom and podcast feeds to JSON and Markdown.

# Actor input Schema

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

Direct links to PDF files (redirects are followed). arXiv /abs/ pages, Google Drive file links and Dropbox share links are converted to their download links automatically.

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

'document' = one row per PDF with the full Markdown (page markers like  included). 'pages' = one row per page. 'chunks' = RAG-ready chunks, each carrying its page range, section headings and a ready-made citation.

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

Only for 'chunks' output. Target maximum characters per chunk (about 4 characters = 1 token, so 1500 is roughly 375 tokens). Chunks break at headings, paragraphs and sentences, never mid-word; tables are split by rows with the header repeated.

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

Only for 'chunks' output. Characters of trailing context (whole sentences) repeated at the start of the next chunk. No overlap is added across a new section heading. Max half the chunk size.

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

Convert (and pay for) only these pages, e.g. '1-5, 8, 12-' ('12-' means page 12 to the end). Leave empty for all pages. Page numbers are the PDF's physical page order, starting at 1.

## `maxPagesPerPdf` (type: `integer`):

Safety cap. Larger PDFs are converted up to this many pages and the row is marked 'partial' with a warning. Also caps what one PDF can cost you.

## `maxPdfSizeMb` (type: `integer`):

Files larger than this are skipped with a TOO_LARGE error row and are not charged.

## `timeoutPerPdfSecs` (type: `integer`):

Conversion of a single PDF is stopped after this many seconds and reported as a TIMEOUT error row (not charged). Raise it for very long documents on low memory.

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

Insert an HTML comment like  before each page's content, so every passage can be cited to its page. Comments are invisible when the Markdown is rendered.

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

Detect ruled tables and aligned number columns (for example financial statements) and output them as Markdown tables instead of jumbled text lines.

## `detectColumns` (type: `boolean`):

Detect two-column pages (research papers, journals, newsletters) and read the left column before the right column instead of mixing lines across columns.

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

Drop running headers, footers and page numbers that repeat on many pages, so they don't pollute paragraphs and chunks.

## `includePages` (type: `boolean`):

Only for 'document' output. Adds a 'pages' array with each page's Markdown, status (text / needs_ocr / empty), headings and table count.

## `includePlainText` (type: `boolean`):

Only for 'document' output. Adds a 'text' field with the Markdown symbols and page markers stripped.

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

Also save each PDF's Markdown as a downloadable .md file in the run's key-value store; its link is in 'markdownFileUrl'. Very large documents are always saved as files.

## `pdfPassword` (type: `string`):

Password for protected PDFs, used for every PDF in the run. Only for documents you are allowed to open.

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

How many PDFs are converted at the same time. Keep 1 at the default 1 GB memory (a long scanned PDF can use about 400 MB); use 2-4 with 4 GB for big batches.

## `proxyConfiguration` (type: `object`):

Optional. Only needed when a site blocks direct downloads. Leave off for normal public PDF links.

## `compactOutput` (type: `boolean`):

Return essential content, source identifiers and errors only; omit bulky optional metadata.

## `timeBudgetSecs` (type: `integer`):

Stop early and keep completed results before this budget or the platform deadline, whichever comes first.

## Actor input object example

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762"
  ],
  "outputFormat": "document",
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "pageRange": "",
  "maxPagesPerPdf": 20,
  "maxPdfSizeMb": 100,
  "timeoutPerPdfSecs": 300,
  "pageMarkers": true,
  "detectTables": true,
  "detectColumns": true,
  "removeHeadersFooters": true,
  "includePages": false,
  "includePlainText": false,
  "saveMarkdownFiles": true,
  "pdfPassword": "",
  "maxConcurrency": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "compactOutput": true,
  "timeBudgetSecs": 120
}
```

# Actor output Schema

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

Converted documents, pages or chunks in the run's default dataset.

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

Downloadable .md file for each converted PDF.

## `safety` (type: `string`):

Useful result count, partial flag and stop reason; distinguish completed work from early timeout or budget stops.

# 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": [
        "https://arxiv.org/pdf/1706.03762"
    ],
    "outputFormat": "document",
    "chunkSize": 1500,
    "chunkOverlap": 150,
    "pageRange": "",
    "maxPagesPerPdf": 20,
    "maxPdfSizeMb": 100,
    "timeoutPerPdfSecs": 300,
    "pageMarkers": true,
    "detectTables": true,
    "detectColumns": true,
    "removeHeadersFooters": true,
    "includePages": false,
    "includePlainText": false,
    "saveMarkdownFiles": true,
    "pdfPassword": "",
    "maxConcurrency": 1,
    "proxyConfiguration": {
        "useApifyProxy": false
    },
    "compactOutput": true,
    "timeBudgetSecs": 120
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryumoon-data/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": ["https://arxiv.org/pdf/1706.03762"],
    "outputFormat": "document",
    "chunkSize": 1500,
    "chunkOverlap": 150,
    "pageRange": "",
    "maxPagesPerPdf": 20,
    "maxPdfSizeMb": 100,
    "timeoutPerPdfSecs": 300,
    "pageMarkers": True,
    "detectTables": True,
    "detectColumns": True,
    "removeHeadersFooters": True,
    "includePages": False,
    "includePlainText": False,
    "saveMarkdownFiles": True,
    "pdfPassword": "",
    "maxConcurrency": 1,
    "proxyConfiguration": { "useApifyProxy": False },
    "compactOutput": True,
    "timeBudgetSecs": 120,
}

# Run the Actor and wait for it to finish
run = client.actor("ryumoon-data/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": [
    "https://arxiv.org/pdf/1706.03762"
  ],
  "outputFormat": "document",
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "pageRange": "",
  "maxPagesPerPdf": 20,
  "maxPdfSizeMb": 100,
  "timeoutPerPdfSecs": 300,
  "pageMarkers": true,
  "detectTables": true,
  "detectColumns": true,
  "removeHeadersFooters": true,
  "includePages": false,
  "includePlainText": false,
  "saveMarkdownFiles": true,
  "pdfPassword": "",
  "maxConcurrency": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "compactOutput": true,
  "timeBudgetSecs": 120
}' |
apify call ryumoon-data/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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