# PDF to Markdown and Text for LLMs and RAG (`athletic_chill/pdf-to-markdown`) Actor

PDF to Markdown and PDF to text for LLMs and RAG. Extract text from PDF URLs: returns one Markdown document per PDF, with reading-order text, headings and real Markdown tables. Handles multi-column pages. Text-based PDFs only, no OCR.

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

## Pricing

from $2.00 / 1,000 pdf page with texts

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 for LLMs and RAG

Convert PDF to clean Markdown, ready for an LLM or a RAG pipeline. Give it PDF URLs, and it returns one Markdown document per PDF: text in reading order, headings where they can be detected, and tables as real Markdown tables instead of a jumble of numbers.

Plain PDF to text tools flatten a table into loose lines, so a model can't tell which number belongs to which row. This Actor keeps the rows and columns. It also reads multi-column pages column by column, and takes page numbers and repeated running headers out of the text.

**This Actor reads text-based PDFs only.** It doesn't do OCR, so scanned PDFs and photos of documents return no text. Those pages are reported and not charged.

### What you get

One dataset record per PDF:

| Field | Meaning |
|---|---|
| `recordType` | `document`, or `issue` for something that was not read or not charged |
| `sourceUrl` | The PDF the document came from |
| `markdown` | The whole document as Markdown |
| `pageCount` | Pages in the PDF |
| `pagesCharged` | Pages with extracted text, charged for this PDF |
| `tableCount` | Markdown tables in the document |

```json
{
  "recordType": "document",
  "sourceUrl": "https://example.com/report.pdf",
  "pageCount": 2,
  "pagesCharged": 2,
  "tableCount": 1,
  "markdown": "# Annual Report\n\nSales grew in every region.\n\n| Region | Sales |\n| --- | --- |\n| North | 120 |\n| South | 95 |"
}
```

Anything that wasn't read comes back as an `issue` record with a `reason`, a `message` and the `pages` it applies to, in the same dataset. The run also saves a `SUMMARY` record with pages read, pages charged and PDFs that failed.

### Input example

```json
{
  "pdfUrls": ["https://example.com/report.pdf"],
  "pages": "1-3",
  "maxPages": 50
}
```

- `pdfUrls`: direct http or https links. Up to 100 per run.
- `pages`: optional, such as `1-3,7`. Applies to every PDF.
- `maxPages`: most pages to read in the whole run, across all PDFs. Default 50. It also caps the cost of a run.

The links must be reachable without logging in. Links to private or local addresses aren't accepted.

### Pricing

You pay $0.002 per page with extracted text, plus Apify's small start fee of $0.00005 per run. These are not charged:

- scanned or blank pages
- pages that couldn't be read
- pages past `maxPages` or your spending limit
- PDFs that failed to download or aren't PDFs

Each of these appears in the results as an `issue` record. If your spending limit runs out, the Actor stops at that page and reports the pages it didn't read.

### Limits

- Text-based PDFs only. No OCR.
- 20 MB per PDF, 60 seconds per download, and 2 minutes of processing per PDF.
- Password-protected PDFs aren't supported.
- A document's Markdown must fit in one dataset record, about 8 MB. Larger ones are reported and not charged.

### What works well, and what doesn't

We tested it on 20 real PDFs: three multi-column documents, invoices, financial statements, government reports and a police records report. We read the output against the source pages. Two-column papers and three-column government text came out in the right order on full pages. Tables that run over several pages are joined: a 16-page report came out with 2 tables, not one per page.

Known weak spots:

- **Text inside images:** it isn't read. A page with a large image gets a `large_images` issue, and it's still charged if it has any text. One test PDF had five such pages, with whole forms stored as pictures.
- **Text in figures:** labels in diagrams aren't told apart from body text, so they appear as stray lines, and can come out as false headings. Line chart labels are removed, but bar chart labels aren't.
- **Complex tables:** a table with merged cells, nested brackets or math can come out broken or in two pieces. In one paper, a large architecture table came out split and hard to read.
- **Small tables:** a borderless table with fewer than three rows of figures stays as plain lines of text.
- **Table headers:** a borderless table's header can land in the wrong columns, or stay above the table as a text line.
- **Close columns:** two table columns with a narrow gap can end up in one cell, such as `€ 399,00 21%`.
- **Invoices:** blocks set side by side, such as billing and shipping addresses, can be merged line by line. This doesn't count as a multi-column page. All the text is there, but the two addresses are interleaved.
- **Short columns:** a few lines of columns under a figure can be read straight across instead of column by column.
- **Fixed-width reports:** they come out as lines of text, not as tables.
- **Headings:** they are worked out from font size and bold text, not from the PDF's structure. Author names often come out as headings, and some real headings are missed. Forms and invoices keep only their largest text as a heading.
- **Footnotes:** they move to the end of their page, and aren't linked to their marks. Footnote marks in the text stay as plain numbers.
- **Formulas:** math comes out as flat text, and some symbols are lost.
- **Repeated lines:** a repeated header or footer stays once, where it first appears, because it may be the only place a company name or a table title is printed. Lines that change on every page, like page numbers, are removed. Anything that repeats outside the top and bottom of the page stays.
- **Languages:** only left-to-right text was tested.

Documents are processed in memory and aren't written to logs. The Markdown is saved to your run's dataset, as with any Actor.

Built with Claude, an AI, and tested with real PDFs.

# Actor input Schema

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

Required. An array of URL strings, such as \["https://example.com/report.pdf"]. Each must be a direct http or https link that returns a text-based PDF without a login. Up to 100 URLs per run and 20 MB per file.

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

Optional. A string of page numbers and ranges to read in every PDF, such as "1-3,7". Omit it to read from page 1.

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

Optional. A whole number from 1 to 500, default 50. The most pages to read across all PDFs in the run, in order. Pages past it are not read and are listed in an issue record. Only pages with extracted text are charged, so this also caps the cost.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/background-checks.pdf"
  ],
  "maxPages": 50
}
```

# Actor output Schema

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

The Markdown of each PDF, with its page and table counts.

## `summary` (type: `string`):

Pages read, pages charged, and PDFs that failed.

# 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": [
        "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/background-checks.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("athletic_chill/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": ["https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/background-checks.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("athletic_chill/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": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/background-checks.pdf"
  ]
}' |
apify call athletic_chill/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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