# PDF to Markdown & Text Extractor (with OCR) (`busy_courtyard/pdf-to-markdown`) Actor

Convert PDFs from URLs into clean Markdown and plain text, page by page, with optional OCR for scanned pages. Output goes to the dataset and a Markdown file per PDF.

- **URL**: https://apify.com/busy_courtyard/pdf-to-markdown.md
- **Developed by:** [Cole Cassel](https://apify.com/busy_courtyard) (community)
- **Categories:** Developer tools, AI
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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 & Text Extractor (with OCR)

Convert PDFs from any public URL into **clean Markdown or plain text**, ready for LLMs, RAG pipelines, search indexes or plain reading. It keeps the structure:

- headings, detected from font size;
- bullet lists;
- real tables, as Markdown tables;
- page markers.

Scanned pages with no text layer are automatically run through **Tesseract OCR**.

You pay **$0.0008 per text page and $0.004 per OCR page**. Empty pages are free.

### What it does

- Downloads each PDF, up to 50 MB by default. It checks that the file really is a PDF, follows redirects, retries on timeouts and on 429/5xx, and is polite (`PdfToMarkdownBot/1.0`, robots.txt respected).
- Reads every page with PyMuPDF:
  - **Markdown:** larger fonts become `#`/`##`/`###` headings, short all-bold blocks become `####` headings, bullets become list items, and hyphenated line breaks are joined. Detected tables become Markdown tables, with junk "tables" from form layouts filtered out.
  - **Text:** clean plain text in reading order.
- Decides per page whether OCR is needed (`ocrMode: auto`): if a page has fewer than `minTextChars` characters of real text, it is rendered at `ocrDpi` and OCR'd. You can force `always` or `never`.
- Returns one dataset row per document, or per page, with metadata and per-page statistics. The full Markdown/text file is also saved in the key-value store, so huge documents are never cut off.

### Use cases

- Feeding reports, papers and manuals into ChatGPT, Claude or a RAG/vector database.
- Making scanned public archives (government reports, old manuals) searchable.
- Bulk-converting public research papers, standards and policy documents to Markdown for notes or a wiki.
- Monitoring public PDF publications by extracting text for diffing or keyword alerts.

### Input

```json
{
  "startUrls": [
    { "url": "https://arxiv.org/pdf/1706.03762" },
    { "url": "https://nvlpubs.nist.gov/nistpubs/CSWP/NIST.CSWP.29.pdf" },
    { "url": "https://ntrs.nasa.gov/api/citations/19930091108/downloads/19930091108.pdf" }
  ],
  "outputFormat": "markdown",
  "ocrMode": "auto"
}
```

| Field | Default | Notes |
|---|---|---|
| `outputFormat` | `markdown` | `markdown`, `text` or `both` |
| `outputMode` | `document` | `document` gives one row per PDF; `page` gives one row per page |
| `ocrMode` | `auto` | `auto` OCRs only pages without text; `always` or `never` |
| `ocrLanguage` / `ocrDpi` | `eng` / 200 | The prebuilt image includes English |
| `minTextChars` | 25 | Below this, a page counts as scanned in `auto` mode |
| `pageRange` | all | For example `1-3, 7, 10-` |
| `maxPages` / `maxFileMb` | 500 / 50 | |
| `respectRobotsTxt` | `true` | |
| `maxConcurrency` / `minDelayBetweenRequestsSecs` | 2 / 1 s | |
| `requestTimeoutSecs` / `maxRetries` | 120 s / 2 | |

### Output

A real document row from a test run on 2026-10-18, with the Markdown and the page list shortened here:

```json
{
  "url": "https://arxiv.org/pdf/1706.03762",
  "ok": true,
  "fileName": "1706.03762",
  "creator": "LaTeX with hyperref",
  "producer": "pdfTeX-1.40.25",
  "pageCount": 15,
  "pagesProcessed": 15,
  "textPages": 15,
  "ocrPages": 0,
  "emptyPages": 0,
  "pages": [
    { "pageNumber": 1, "method": "text", "chars": 2857 },
    { "pageNumber": 2, "method": "text", "chars": 4255 }
  ],
  "charCount": 39757,
  "wordCount": 6155,
  "markdown": "<!-- page 1 -->\nProvided proper attribution is provided, Google hereby grants permission … \n\n## Attention Is All You Need\n\nAshish Vaswani∗\n\nGoogle Brain …\n\n### Abstract\n\nThe dominant sequence transduction models are based on …",
  "contentTruncated": false,
  "markdownUrl": "https://api.apify.com/v2/key-value-stores/<store-id>/records/1706-dd93e904.md",
  "processingTimeMs": 1795,
  "attempts": 1
}
```

A real OCR page row from a 1930s NACA scanned report (`outputMode: page`, `ocrMode: always`), shortened:

```json
{
  "url": "https://ntrs.nasa.gov/api/citations/19930091108/downloads/19930091108.pdf",
  "ok": true,
  "pageNumber": 4,
  "pageCount": 61,
  "method": "ocr",
  "chars": 2215,
  "text": "NATIONAL ADVISORY COMMITTEE FOR AERONAUTICS\nNAVY BUILDING, WASHINGTON, D.C.\n\n(An independent Government establishment, created by act of Congress approved March 3, 1915, …"
}
```

Failures become free rows with `ok: false` and an `error.code`:

- `NOT_A_PDF`, `HTTP_404` and other `HTTP_<status>` codes;
- `TOO_LARGE`, `ENCRYPTED_PDF`, `BROKEN_PDF`;
- `ROBOTS_DISALLOWED`, `TIMEOUT`, `NETWORK_ERROR`, `DNS_ERROR`, `PRIVATE_ADDRESS`, `INVALID_URL`.

Test run results:

- **Main run:** 4 public PDFs (arXiv, IRS W-9, NIST, NASA), 114 pages, in 18 seconds. 112 pages came from the text layer, 1 needed OCR and 1 was empty and free. Two bad URLs were recorded as `NOT_A_PDF` and `HTTP_404`.
- **OCR run:** 10 pages of the scanned NASA report in 27 seconds.

### Pricing

Pay per event:

| Event | Price |
|---|---|
| Page extracted from text layer | **$0.0008** ($0.80 per 1,000) |
| Page extracted with OCR | **$0.004** ($4 per 1,000) |
| Run start (Apify standard) | $0.00005 per GB of memory |

Empty pages, failed files and pages outside `pageRange` are free. Use `pageRange`, `maxPages` or a maximum cost per run to cap spending.

### Limits

- OCR quality depends on the scan. Expect typical OCR mistakes on old or low-quality scans, especially in names and numbers. Only English (`eng`) is installed in the image.
- OCR is CPU-heavy. For scan-heavy jobs, give the run 2–4 GB of memory, which on Apify also means more CPU, so it finishes faster.
- Multi-column layouts can sometimes interleave columns. Tables without ruling lines may come out as text rather than a Markdown table.
- Password-protected PDFs aren't supported (`ENCRYPTED_PDF`).
- Public URLs only: no logins or cookies.

### FAQ

**Will it cut off long documents?**
The full output is always saved as a `.md`/`.txt` file in the key-value store (`markdownUrl`/`textUrl`). Only the copy inside the dataset row is capped at 2 million characters, and `contentTruncated` tells you when that happened.

**How do I know which pages were OCR'd?**
Each entry in `pages` has `method`: `text`, `ocr` or `empty`. `ocrPages` gives the total.

**Can I process only the first pages?**
Yes, for example `"pageRange": "1-5"`. You are only charged for the pages processed.

**Does it handle forms such as tax forms?**
Yes. Text is extracted, and layout boxes that aren't real tables are filtered out instead of producing junk tables.

# Actor input Schema

## `startUrls` (type: `array`):

Public links to PDF files. Each PDF becomes one dataset row, or one row per page in 'page' mode.

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

Markdown keeps headings, lists and tables. Text is the plain reading-order text.

## `outputMode` (type: `string`):

'document' gives one row per PDF with the full content. 'page' gives one row per page, which suits RAG chunking.

## `ocrMode` (type: `string`):

'auto' OCRs only pages with no usable text layer (scans). 'always' OCRs every page. 'never' uses the text layer only. OCR pages cost more.

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

Tesseract language code. This build includes English ('eng').

## `ocrDpi` (type: `integer`):

Higher reads small print better but is slower.

## `minTextChars` (type: `integer`):

In auto mode, a page with fewer characters than this that also contains an image counts as scanned and is OCR'd.

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

Optional, e.g. '1-3,7,10-'. Leave empty for all pages.

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

Safety cap for very long documents.

## `maxFileMb` (type: `integer`):

Bigger files are rejected while downloading.

## `respectRobotsTxt` (type: `boolean`):

Skip PDFs that the site's robots.txt disallows. Turn it off only for files you own or have permission to fetch.

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

Downloads run in parallel. Conversion runs one PDF at a time.

## `minDelayBetweenRequestsSecs` (type: `number`):

A robots.txt Crawl-delay overrides it if it is longer.

## `requestTimeoutSecs` (type: `integer`):

Maximum time for downloading one file.

## `maxRetries` (type: `integer`):

Retries for timeouts, 429 and 5xx errors. Retries use exponential backoff.

## `selfTest` (type: `boolean`):

Ignore the other fields (startUrls is still required by the form, any URL will do) and convert page 1 of the IRS Form W-9, then fail the run if the Markdown is missing or empty. Meant for a daily health-check schedule.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ],
  "outputFormat": "markdown",
  "outputMode": "document",
  "ocrMode": "auto",
  "ocrLanguage": "eng",
  "ocrDpi": 200,
  "minTextChars": 25,
  "maxPages": 500,
  "maxFileMb": 50,
  "respectRobotsTxt": true,
  "maxConcurrency": 2,
  "minDelayBetweenRequestsSecs": 1,
  "requestTimeoutSecs": 120,
  "maxRetries": 2,
  "selfTest": false
}
```

# Actor output Schema

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

One row per result in the default dataset.

## `files` (type: `string`):

One Markdown (or text) file per PDF in the default key-value store.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("busy_courtyard/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 = { "startUrls": [{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("busy_courtyard/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 '{
  "startUrls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ]
}' |
apify call busy_courtyard/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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