# PDF to Markdown & Text Extractor for LLM and RAG (`datagleaner/pdf-to-markdown`) Actor

Convert PDF URLs to clean Markdown or plain text for LLMs, AI agents and RAG. Headings, lists, paragraphs and simple tables, optional ready-to-embed chunks with page numbers. No start fee, $0.002 per PDF, scanned PDFs are not charged.

- **URL**: https://apify.com/datagleaner/pdf-to-markdown.md
- **Developed by:** [Data Gleaner](https://apify.com/datagleaner) (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.40 / 1,000 pdf 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 & Text Extractor for LLM and RAG

**Give it PDF links, get clean Markdown, plain text and ready-to-embed chunks back.** Headings become `#` titles, wrapped lines become real paragraphs, bullet lists stay lists, simple tables become Markdown tables, and page headers, footers and page numbers are removed. Built for RAG ingestion, AI agents and LLM pipelines. No browser, no API key, no start fee.

**Price: $0.002 per PDF** ($2 per 1,000 documents), no start fee. PDFs that fail to download or have no text (scans) are returned with the reason and **not charged**. See Pricing below.

### Why use this one

- **Cheapest per document.** One flat price per PDF, whether it has 2 pages or 200. No start fee and no per-page charge.
- **Real Markdown, not a text dump.** Heading levels are guessed from font size, bold and section numbers (`1`, `2.1`, `A.3`); lines are re-flowed into paragraphs, words hyphenated at a line end are re-joined, `-` and `•` lists are kept, and tables with aligned columns are returned as Markdown tables.
- **RAG chunks included.** Set `chunkSize` and each document also returns a `chunks` array: pieces cut at paragraph and sentence boundaries, with overlap, each with its `pageStart` and `pageEnd` so an answer can cite the page.
- **Fast.** Text extraction reads the file in-process, so a 100-page report converts in about two seconds.
- **Safe on bad input.** One broken link never stops the run: every URL returns one row, with an `error` that says what happened.
- **Honest limit:** no OCR yet. A scanned PDF is returned with `hasTextLayer: false` and is not charged.

### Minimal input

```json
{"urls": [{"url": "https://arxiv.org/pdf/1706.03762"}], "outputFormat": "markdown", "chunkSize": 1000}
```

Start it with no input and it converts one built-in sample PDF (the 15-page arXiv paper 1706.03762) and says so in the run's status message.

### What does PDF to Markdown do?

For each link it downloads the file (up to 50 MB, redirects followed, a real PDF check on the first bytes), reads the text layer with every character's position and font, and rebuilds the document structure. Typical uses:

- **RAG ingestion.** Turn a folder of reports, papers or manuals into Markdown chunks with page numbers for a vector database.
- **AI agents.** Give an agent a tool that reads any PDF link into text it can quote and reason over.
- **Research papers.** Convert arXiv and journal PDFs to Markdown with section headings, ready to summarise or compare.
- **Contracts and policies.** Pull terms, clauses and numbered sections into text for review or clause extraction.
- **Reports and filings.** Annual reports, government and regulatory reports, white papers and earnings releases as searchable text.
- **Knowledge bases and search.** Index documentation PDFs by title, page and section.

### Input

| Field | Type | What it does |
|---|---|---|
| `urls` | list of links | The PDF files to convert. Leave empty for the built-in sample. |
| `outputFormat` | choice | `markdown` (default), `text` or `both`. |
| `maxPagesPerPdf` | number | Convert only the first N pages, default 200. `pageCount` still shows the full length. |
| `pageRange` | text | Optional pages, 1-based: `1-5`, `8`, `10-12`, `3-` (page 3 to the end). |
| `chunkSize` | number | `0` (default) returns no chunks. Set about `1000` characters (roughly 250 tokens) to also get `chunks`. |
| `chunkOverlap` | number | Characters repeated between neighbouring chunks, default 100. |
| `includePageBreaks` | true/false | Add a `--- Page 3 ---` line where each page starts. |
| `removeHeadersFooters` | true/false | Drop running headers, footers and page numbers, default true. |
| `maxPdfs` | number | Process at most this many of the URLs; also your cost cap. |
| `maxFileSizeMb` | number | Skip larger files (default 50). |
| `maxConcurrency` | number | PDFs downloaded in parallel, default 4. |
| `proxyConfiguration` | object | Optional Apify Proxy for sites that refuse the download. Default is no proxy. |

```json
{
  "urls": [
    {"url": "https://arxiv.org/pdf/1706.03762"},
    {"url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf"}
  ],
  "outputFormat": "both",
  "pageRange": "1-10",
  "chunkSize": 1000,
  "chunkOverlap": 150,
  "includePageBreaks": true,
  "maxPdfs": 50
}
```

### Output

One dataset row per PDF.

| Field | Meaning |
|---|---|
| `url`, `finalUrl` | The link you gave and where the file was finally downloaded from. |
| `fileName` | From the server or the link. |
| `title` | The PDF's title metadata, else the first heading. |
| `author` | From the PDF metadata, if present. |
| `pageCount`, `pagesProcessed` | Pages in the file and pages converted. |
| `wordCount`, `charCount` | Size of the extracted text. |
| `hasTextLayer` | `false` for scanned or image-only PDFs. |
| `markdown`, `text` | The content, depending on `outputFormat`. |
| `chunks` | When `chunkSize` > 0: `{index, text, pageStart, pageEnd}` objects. |
| `metadata` | `producer`, `creator`, `creationDate`, `modificationDate` (ISO 8601). |
| `error` | `null` on success, otherwise the reason (download refused, not a PDF, scanned, password-protected). |
| `scrapedAt`, `input` | When it ran and the settings used. |

```json
{
  "url": "https://arxiv.org/pdf/1706.03762",
  "finalUrl": "https://arxiv.org/pdf/1706.03762",
  "fileName": "1706.03762v7.pdf",
  "title": "Attention Is All You Need",
  "author": null,
  "pageCount": 15,
  "pagesProcessed": 15,
  "wordCount": 5995,
  "charCount": 38771,
  "hasTextLayer": true,
  "markdown": "# Attention Is All You Need\n\n## Abstract\n\nThe dominant sequence transduction models are based on complex recurrent or convolutional neural networks ...\n\n## 1 Introduction\n\n...",
  "chunks": [
    {"index": 0, "text": "# Attention Is All You Need\n\nAshish Vaswani ...", "pageStart": 1, "pageEnd": 1},
    {"index": 1, "text": "...", "pageStart": 1, "pageEnd": 2}
  ],
  "metadata": {"producer": "pdfTeX-1.40.25", "creator": "LaTeX with hyperref", "creationDate": "2024-04-10T21:11:43+00:00", "modificationDate": "2024-04-10T21:11:43+00:00"},
  "error": null,
  "scrapedAt": "2026-10-22T06:00:00+00:00",
  "input": {"url": "https://arxiv.org/pdf/1706.03762", "outputFormat": "markdown", "pageRange": null, "maxPagesPerPdf": 200, "chunkSize": 1000, "includePageBreaks": false}
}
```

### Pricing

**$0.002 per PDF converted ($2 per 1,000), no start fee**, billed through the pay-per-event event `pdf-document`. Higher Apify plans get lower prices: $0.0017 on Silver and $0.0014 on Gold. This price applies from 22 October 2026. A PDF is billed once when its text is extracted, however many pages it has and whether or not you ask for chunks. Nothing is charged for a link that fails to download, is not a PDF, is password-protected or has no text layer.

Worked examples:

- 1 research paper (any length) = **$0.002**.
- 100 contracts or reports = **$0.20**.
- A 5,000-document knowledge base = **$10.00**, less if some are scans.
- 1,000 links where 80 are scanned or dead = 920 billed = **$1.84**.

Set `maxPdfs` and the run cannot cost more than that many times $0.002. If you also set a maximum charge on the run, it stops there too.

### Limits

- **No OCR yet.** Scanned and image-only PDFs have no text to read; they are returned with `hasTextLayer: false` and an explanation, and are not charged. Text embedded in images inside a normal PDF is not read.
- **Password-protected PDFs are skipped** (returned with an error, not charged).
- **Layout is inferred.** Headings, lists and tables are guessed from fonts and positions, so unusual designs, complex multi-row table headers, equations and figure labels can come out as plain lines of text. Multi-column pages follow the reading order stored in the PDF, which is correct for most papers and reports.
- **Size.** Files over `maxFileSizeMb` (default 50 MB) are skipped. Very long documents are capped by `maxPagesPerPdf`.
- **Some sites refuse automated downloads** (HTTP 403). That row has the reason and is not charged; try `proxyConfiguration` or another copy of the file.

### Use it from n8n, Make, Zapier or an AI agent

Actor ID: `datagleaner/pdf-to-markdown`

Minimal input:

```
{"urls": [{"url": "https://arxiv.org/pdf/1706.03762"}], "chunkSize": 1000}
```

Each tool below runs this Actor with your own Apify API token.

- **n8n:** add the **Apify** node (`@apify/n8n-nodes-apify`). Choose **Run an Actor and get dataset**, set Actor to `datagleaner/pdf-to-markdown` and paste the input above.

- **Make:** use the Apify app's **Run an Actor** module, then **Get Dataset Items**. **Watch Actor Runs** can trigger a scenario when a run finishes.

- **Zapier:** use the Apify action **Run Actor**, then **Fetch dataset items**. The trigger **Finished Actor run** starts a Zap when a run ends.

- **AI agents (MCP):** connect to `https://mcp.apify.com/?tools=datagleaner/pdf-to-markdown`. In Claude Code:

  ```
  claude mcp add --transport http apify "https://mcp.apify.com/?tools=datagleaner/pdf-to-markdown"
  ```

  Then run `/mcp` to sign in to Apify in your browser. Other clients can sign in with OAuth or send the header `Authorization: Bearer YOUR_APIFY_TOKEN`. Then ask the agent in plain words, for example:

  > Convert this PDF to Markdown and summarise section 3: https://arxiv.org/pdf/1706.03762

- **LangChain (Python):**

```python
## pip install langchain-apify, then set APIFY_TOKEN in your environment
import json
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("datagleaner/pdf-to-markdown")
result = tool.invoke({"run_input": json.loads('{"urls": [{"url": "https://arxiv.org/pdf/1706.03762"}], "chunkSize": 1000}')})
```

- **LlamaIndex (Python):** run the Actor with the Apify client and load the rows as documents:

```python
## pip install apify-client llama-index
from apify_client import ApifyClient
from llama_index.core import Document
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("datagleaner/pdf-to-markdown").call(run_input={"urls": [{"url": "https://arxiv.org/pdf/1706.03762"}]})
docs = [Document(text=row["markdown"], metadata={"title": row["title"], "url": row["url"], "pages": row["pageCount"]})
        for row in client.dataset(run["defaultDatasetId"]).iterate_items() if row.get("markdown")]
```

### FAQ

**Do I need an API key or login?** No, only an Apify account. The Actor downloads public PDF links; it cannot open files behind a login.

**What if I start it with no links?** It converts one built-in sample PDF (arXiv 1706.03762, 15 pages) so you can see the output, and says so in the run's status message.

**How are chunks cut?** At paragraph boundaries first, then sentence or word boundaries for very long paragraphs, to about `chunkSize` characters, with `chunkOverlap` characters repeated from the previous chunk. Each chunk carries the first and last PDF page it covers.

**Why is a table not a table?** Tables are detected when three or more consecutive lines have the same number of aligned columns. Merged cells, multi-line cells and tables drawn as images are returned as text.

**What happens to a scanned PDF?** It comes back as a row with `hasTextLayer: false`, the page count and an explanation in `error`, and it is not charged. OCR is planned.

**Can I convert only some pages?** Yes, `pageRange` (for example `1-5,9`) and `maxPagesPerPdf`.

### Responsible use

This Actor converts PDF files you point it to. Only submit documents you are allowed to download and process, and respect the terms of the sites that host them. If the documents contain personal data, the obligations for processing it are yours.

# Actor input Schema

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

Links of the PDF files to convert, one per line. Each link must be a public file that downloads without a login. Leave empty to convert one built-in sample PDF (arXiv 1706.03762).

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

markdown: headings, lists, paragraphs and tables as Markdown (best for LLMs). text: plain text with paragraphs separated by blank lines. both: two fields per PDF.

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

Only the first N pages (after the page range) are converted; pageCount still shows the full length. A cost and speed cap for very long documents.

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

Optional. Convert only these pages, 1-based: for example 1-5, 8, 10-12 or 3- (page 3 to the end). Empty means all pages.

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

0 returns no chunks. Set it (for example 1000) to also get a `chunks` array: pieces of about this many characters, cut at paragraph or sentence boundaries, each with its page numbers, ready for embedding. About 4 characters make 1 token.

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

How many characters of the previous chunk each new chunk repeats, so a sentence cut at a boundary is still found. Used only when chunk size is above 0, and never more than half the chunk size.

## `includePageBreaks` (type: `boolean`):

Insert a line like `--- Page 3 ---` where each PDF page starts, to cite pages from the output.

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

Drop text that repeats near the top or bottom edge of most pages (running titles, 'Page 3 of 20'), which otherwise pollutes embeddings.

## `maxPdfs` (type: `integer`):

Upper bound on how many of the URLs are processed in this run, also your cost cap: each converted PDF is billed once.

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

Larger files are skipped with an error and are not billed.

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

How many PDFs are downloaded at the same time. Text extraction itself runs one PDF at a time.

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

Optional. The default is no proxy, which works for most public PDFs. Enable Apify Proxy if a site refuses the download (HTTP 403).

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://arxiv.org/pdf/1706.03762"
    },
    {
      "url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf"
    }
  ],
  "outputFormat": "markdown",
  "maxPagesPerPdf": 200,
  "chunkSize": 0,
  "chunkOverlap": 100,
  "includePageBreaks": false,
  "removeHeadersFooters": true,
  "maxPdfs": 10,
  "maxFileSizeMb": 50,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

# 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"
        },
        {
            "url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf"
        }
    ],
    "maxPdfs": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datagleaner/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" },
        { "url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf" },
    ],
    "maxPdfs": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("datagleaner/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"
    },
    {
      "url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf"
    }
  ],
  "maxPdfs": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call datagleaner/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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