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

Turn PDFs into clean text and Markdown for LLM and RAG pipelines. Headings from font size and weight, paragraphs reflowed, columns in reading order, repeated headers removed. Optional per-page text and embedding-sized chunks with page range and heading path. Flags scanned PDFs; no OCR.

- **URL**: https://apify.com/antsonahill/pdf-to-markdown-extractor.md
- **Developed by:** [Anton Sajin](https://apify.com/antsonahill) (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 $4.00 / 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.

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 and Text Extractor

Turn PDFs into clean text and Markdown that an LLM can read and a RAG pipeline can index.

A PDF stores letters at positions on a page, not paragraphs. Copy the text out naively and you get lines cut in mid-sentence, words split by hy-phens, two columns shuffled into each other, and a page number in the middle of every thought. This Actor puts the text back together: headings become headings, wrapped lines become paragraphs, columns are read in order, running headers and footers are removed, and the result can be cut into chunks that know which page and which section they came from.

Give it PDF links, uploaded files, or PDFs another Actor saved. Get one result per PDF.

**It does not do OCR.** A scanned PDF has no text to extract. The Actor tells you so in the result (`isScanned: true`) instead of returning an empty string, and does not charge for it.

### What you get for each PDF

- **Markdown** with headings inferred from font size and weight, bullet and numbered lists, simple tables, and paragraphs reflowed across line breaks, columns and pages.
- **Plain text** with the same reading order and paragraphs, without Markdown syntax.
- **Document properties** as the file states them: title, author, subject, keywords, creating program, dates, language.
- **Page count**, and which pages have no text layer.
- **An outline**: every heading with its level and page.
- Optional **text per page**, for page-level citations.
- Optional **chunks for embedding**: sized in characters or approximate tokens, never crossing a heading, broken between paragraphs where possible, each with its page range and heading path.

### What you can do with it

- **Feed documents to an LLM.** Reports, papers, manuals and contracts as Markdown, with the structure a model needs to find its way around.
- **Build a RAG index.** Chunks arrive with `pageStart`, `pageEnd` and `headingPath`, so an answer can cite "section 3.2, page 19".
- **Process what a crawler found.** Point the Actor at the key-value store where a crawl saved its PDFs.
- **Check a batch of files.** Find out which PDFs are scans, which are password-protected, and which links do not lead to a PDF at all, without paying for them.

### Input example

```json
{
    "pdfUrls": [
        "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
        "https://arxiv.org/abs/2302.13971"
    ],
    "pageRange": "1-30",
    "includeChunks": true,
    "chunkSize": 1200,
    "chunkOverlap": 150
}
```

#### Where the PDFs come from

| Field | Use |
| --- | --- |
| `pdfUrls` | Links to PDF files. Redirects are followed, so DOI links work. A link to an arXiv abstract page or a GitHub file page is pointed at the PDF itself. |
| `pdfFiles` | Files uploaded from your computer in the Apify Console. |
| `keyValueStoreId` | A key-value store that holds PDFs. Every record whose key ends in `.pdf` is read. |
| `recordKeys` | Read only these records of the store, whatever their keys look like. |

#### Options

| Field | Default | What it does |
| --- | --- | --- |
| `pageRange` | all pages | Pages to read, for example `1-5, 8, 12-`. |
| `maxPagesPerDocument` | no limit | Stop reading each PDF after this many pages. |
| `password` | none | Opens PDFs that need a password. PDFs that only restrict printing or copying open without one. |
| `removeHeadersFooters` | `true` | Removes running headers, footers and page numbers. |
| `includePages` | `false` | Adds `pages`: the plain text of every page. |
| `includeChunks` | `false` | Adds `chunks`. |
| `chunkSize` | `1000` | Largest chunk, in characters or tokens. |
| `chunkOverlap` | `100` | How much of a chunk is repeated at the start of the next one. |
| `chunkUnit` | `characters` | `characters` or `tokens`. Tokens are estimated at four characters each. |

### Output

One item per PDF. This is from a real run on the NIST AI Risk Management Framework with `includePages` and `includeChunks` on. Long strings are cut short and the arrays show a few of their entries:

```json
{
    "id": "88fe07c03eb03177",
    "url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
    "loadedUrl": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
    "status": "ok",
    "errorCode": null,
    "errorMessage": null,
    "fileName": "NIST.AI.100-1.pdf",
    "fileSizeBytes": 1946127,
    "fileSha256": "7576edb531d9848825814ee88e28b1795d3a84b435b4b797d3670eafdc4a89f1",
    "pageCount": 48,
    "pagesProcessed": 48,
    "isScanned": false,
    "pagesWithoutText": [],
    "isEncrypted": false,
    "metadata": {
        "title": "Artificial Intelligence Risk Management Framework (AI RMF 1.0)",
        "author": "National Institute of Standards and Technology",
        "subject": "As directed by the National Artificial Intelligence Initiative Act of 2020 (P.L. 116-283), the goal of the AI RMF is to offer a resource to the organizations designing, developing, deploying, or using AI systems…",
        "keywords": "Artificial Intelligence (AI); AI; AI RMF; AI RMF 1.0; AI systems; trustworthy and responsible AI.",
        "creator": "LaTeX with hyperref",
        "producer": "pdfTeX-1.40.24",
        "creationDate": "2023-01-24T19:45:46.000Z",
        "modificationDate": "2025-06-04T17:01:45.000Z",
        "language": "en",
        "pdfVersion": "1.7"
    },
    "text": "NIST AI 100-1\n\nArtificial Intelligence Risk Management Framework (AI RMF 1.0)\n\n…",
    "markdown": "#### NIST AI 100-1\n\n# Artificial Intelligence Risk Management Framework (AI RMF 1.0)\n\n…",
    "charCount": 104748,
    "wordCount": 15302,
    "tokenEstimate": 26187,
    "headings": [
        { "level": 3, "text": "1. Framing Risk", "page": 9 },
        { "level": 4, "text": "1.1 Understanding and Addressing Risks, Impacts, and Harms", "page": 9 },
        { "level": 4, "text": "1.2 Challenges for AI Risk Management", "page": 10 },
        { "level": 5, "text": "1.2.1 Risk Measurement", "page": 10 }
    ],
    "pages": [
        { "page": 1, "text": "NIST AI 100-1\n\nArtificial Intelligence Risk Management Framework (AI RMF 1.0)", "charCount": 77 }
    ],
    "chunks": [
        {
            "index": 50,
            "text": "#### 3.2 Safe\n\nAI systems should “not under defined conditions, lead to a state in which human life, health, property, or the environment is endangered” (Source: ISO/IEC TS 5723:2022). Safe operation of AI systems is improved through:\n\n- responsible design, development, and deployment practices;\n- clear information to deployers on responsible use of the system;\n…",
            "charCount": 807,
            "tokenEstimate": 202,
            "pageStart": 19,
            "pageEnd": 19,
            "headingPath": [
                "Artificial Intelligence Risk Management Framework (AI RMF 1.0)",
                "3. AI Risks and Trustworthiness",
                "3.2 Safe"
            ]
        }
    ],
    "removedHeadersFooters": ["NIST AI #-#", "AI RMF #.#", "Page #", "Categories", "Subcategories"],
    "warnings": [],
    "fullResultKey": null,
    "chargedEvents": 1,
    "scrapedAt": "2026-10-11T05:36:07.668Z"
}
```

And this is a stretch of the `markdown` of that item, exactly as returned:

```markdown
##### 3.2 Safe

AI systems should “not under defined conditions, lead to a state in which human life, health, property, or the environment is endangered” (Source: ISO/IEC TS 5723:2022). Safe operation of AI systems is improved through:

- responsible design, development, and deployment practices;
- clear information to deployers on responsible use of the system;
- responsible decision-making by deployers and end users; and
- explanations and documentation of risks based on empirical evidence of incidents.

Different types of safety risks may require tailored AI risk management approaches based on context and the severity of potential risks presented. Safety risks that pose a potential risk of serious injury or death call for the most urgent prioritization and most thorough risk management process.
```

`id` is derived from the URL or record you gave, so it is the same on every run. `fileSha256` changes when the file does. Fields that do not apply are `null`.

#### Scanned PDFs

When no page that was read has a text layer, the item has `status: "no_text_layer"`, `isScanned: true`, empty `text`, and a warning that says OCR is needed. When only some pages are scans, the item is `ok`, the text of the other pages is returned, and `pagesWithoutText` lists the scanned ones.

A scan that already went through OCR has a text layer, and that is what you get: as good or as bad as the OCR that made it.

#### Files that cannot be read

A bad file never stops the run. It gets an item with `status: "failed"` and an `errorCode`, is listed in the `SUMMARY` record of the run's key-value store, and is not charged.

| `errorCode` | Meaning |
| --- | --- |
| `not_a_pdf` | The link returns a web page or another kind of file. |
| `download_failed` | The server refused, timed out, or kept failing after four attempts. |
| `too_large` | The file is over 100 MB. |
| `password_required`, `wrong_password` | The PDF needs a password to open. |
| `corrupt_pdf` | The file is damaged beyond what the reader can repair. |
| `no_pages_selected` | The page range lies outside the document. |
| `timeout`, `out_of_memory` | The file was given up on. Running with more memory helps with the second. |
| `invalid_url`, `empty_file`, `record_not_found`, `internal_error` | As named. |

### How the text is rebuilt, and where that stops working

**Headings** are inferred from typography alone: text set larger than the body is a heading, bold text at body size on a line of its own is a lower-level heading, and section numbers such as `3.2.1` set the depth. The largest heading on the first page is level 1. This works well on papers, reports and manuals. It finds nothing in a document whose headings look like its body text, and it sometimes promotes a bold line that is not a heading, typically an author name under a title.

**Paragraphs** are reflowed. A word hyphenated at a line end is rejoined, and the rest of the document decides whether the hyphen stays: `trans-` + `former` becomes `transformer`, `self-` + `attention` stays `self-attention` if the document writes it that way elsewhere. A paragraph that runs into the next column or page is joined to its second half when that half starts in lower case; when it starts with a capital the two halves stay separate paragraphs.

**Columns** are found from the empty strips between them and read left to right, with full-width titles and footers kept out. Two-column papers and reports read correctly in our tests. Layouts with boxes, sidebars and text wrapped around pictures can come out in the wrong order.

**Headers, footers and page numbers** are removed only when the same line repeats at the same height in the margin of at least three pages close together. Everything removed is listed in `removedHeadersFooters`. A header that changes on every page is left in. The column headings of a table that continues over several pages can be removed as if they were a running header; the list shows it when that happens, and `removeHeadersFooters: false` turns removal off.

**Lists** with bullets, numbers or letters become Markdown lists, nested by indentation.

**Tables** are best effort. Columns of short cells with clear gaps between them become a Markdown table, including under a header cell that spans columns. Tables with ruled lines but little space, cells with wrapped text, merged cells and tables without visible column gaps come out as rows of text or as a table with misplaced cells. If you need tables as data, check them.

**Footnotes** stay where the page puts them, between the paragraphs around the page break, and footnote marks sit in the text as plain digits.

**Not handled.** Formulas come out as loose symbols. Text inside charts and diagrams comes out as stray labels and sometimes as a small meaningless table. Form field values, comments and attachments are not read. Short text set sideways or at an angle (margin stamps, watermarks, axis labels) is dropped. Right-to-left and vertical scripts are extracted but have not been tested, and their order may be wrong. A PDF's own tags and bookmarks are not used. In a document with very complex vector graphics (maps, dense plots), bold type is no longer recognised after the first such page, so later headings are found by size only.

### Limits

- **File size:** 100 MB per PDF. Larger files are reported as `too_large`.
- **Memory:** the default is 2 GB. A 1,300-page government report (9.8 million characters) with page texts and chunks peaked at 1.1 GB; a 43 MB atlas full of vector maps peaked at 0.7 GB; ordinary papers and reports stay under 0.4 GB. The minimum setting of 1 GB is enough for documents of a few hundred pages.
- **Result size:** Apify dataset items hold about 9 MB. A larger result (a book with page texts and chunks) is saved whole as a JSON record in the run's key-value store; the item then leaves out the largest fields and names the record in `fullResultKey`.
- **Stalls:** a PDF that makes no progress for two minutes is given up on and reported, and the run moves on.

### Speed

Text-heavy documents take about 5 to 15 milliseconds per page on one CPU core, papers full of formulas three to four times that. On our test machine two PDFs of 75 pages together took under 2 seconds end to end, and a 1,653-page statute took 8 seconds. On Apify the default memory gives a run half a CPU core, so the same work takes at least twice as long there.

### Pricing

Pay per PDF: **$4 per 1,000 PDFs** of up to 100 pages each. There is no monthly fee.

A longer PDF counts once per started 100 pages, because reading it takes that much longer: a 250-page report is charged as 3 PDFs ($0.012), a 1,000-page book as 10 ($0.04). `chargedEvents` in every item shows what it cost, and `maxPagesPerDocument` caps it.

You pay nothing for files that fail, for links that are not PDFs, or for scanned PDFs that yield no text.

You can cap what a run may spend with the *Maximum cost per run* setting. The Actor stops cleanly at the limit: a long PDF is read as far as the remaining budget pays for and says so in `warnings`, and PDFs it did not reach are counted in the `SUMMARY` record.

### FAQ

**Can it read scanned PDFs?** No. This version has no OCR. It detects scans and reports them at no charge, so you can send only those to an OCR tool.

**Which library reads the PDFs?** Mozilla's pdf.js. Nothing is sent to an outside service; the files are processed inside the run and are not kept after it.

**How accurate is the token count?** `tokenEstimate` and token-based chunk sizes assume four characters per token. That is close for English prose with OpenAI and Anthropic models and rougher for code and for other languages. Leave some margin under your model's limit.

**Does a chunk ever cross a section?** No. A chunk ends at the next heading. Sections longer than the chunk size are split between paragraphs, then between sentences, and the following chunk starts with the last sentences of the one before, up to `chunkOverlap`.

**Why is my heading a paragraph, or my paragraph a heading?** Headings are guessed from font size and weight; see above. The `headings` array shows what was recognised.

**Something looks wrong.** Open an issue on the Issues tab with the run ID and, if you can, the PDF's URL.

# Actor input Schema

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

Direct links to PDF files, one per line. Redirects are followed. Links to an arXiv abstract page or a GitHub file page are pointed at the PDF itself. A link that returns a web page instead of a PDF is reported and not charged.

## `pdfFiles` (type: `array`):

Upload PDFs from your computer instead of, or in addition to, giving URLs. Each file can be up to 100 MB.

## `keyValueStoreId` (type: `string`):

Read PDFs that another Actor or your own code saved as records in a key-value store, for example files downloaded by a crawler. By default every record whose key ends in .pdf is read.

## `recordKeys` (type: `array`):

Keys of the records to read from the key-value store. Leave empty to read every record whose key ends in .pdf.

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

Pages to read from every PDF, for example 1-5, 8, 12- (12 to the end). Pages are numbered from 1. Leave empty for all pages.

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

Stop reading a PDF after this many pages. A PDF is charged once per started 100 pages, so this also caps what one long file can cost. Leave empty for no limit.

## `password` (type: `string`):

Password for PDFs that need one to open. It is tried on every protected PDF in the run. PDFs that only restrict printing or copying open without it.

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

Drop lines that repeat in the top or bottom margin of three or more pages close together, such as running titles and page numbers. Lines that do not repeat are always kept. The removed lines are listed in the output.

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

Add a pages array with the plain text of each page, for page-level citations. This roughly doubles the size of the result.

## `includeChunks` (type: `boolean`):

Add a chunks array ready for embedding. A chunk never crosses a heading and breaks between paragraphs where it can. Each chunk carries its page range and the headings it sits under.

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

Largest size of a chunk, in the unit chosen below. 1000 characters is about 250 tokens.

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

How much of the end of a chunk is repeated at the start of the next one when a long section is split, in the unit chosen below. At most half the chunk size. Chunks that start at a heading have no overlap.

## `chunkUnit` (type: `string`):

Whether chunk size and overlap are counted in characters or in tokens. Tokens are estimated at four characters each, which is close for English and rougher for code and other languages.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
    "https://arxiv.org/pdf/2302.13971v1"
  ],
  "removeHeadersFooters": true,
  "includePages": false,
  "includeChunks": false,
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "chunkUnit": "characters"
}
```

# Actor output Schema

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

One item per PDF with its text, Markdown, metadata and, if requested, page texts and chunks.

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

Per-PDF status: pages read, events charged, and why any file could not be read.

# 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://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
        "https://arxiv.org/pdf/2302.13971v1"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("antsonahill/pdf-to-markdown-extractor").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://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
        "https://arxiv.org/pdf/2302.13971v1",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("antsonahill/pdf-to-markdown-extractor").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://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.100-1.pdf",
    "https://arxiv.org/pdf/2302.13971v1"
  ]
}' |
apify call antsonahill/pdf-to-markdown-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,antsonahill/pdf-to-markdown-extractor"
        }
    }
}
```

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/QN2FRolvfnFedcdPo/builds/doSrNN6WTcDdQ9nUr/openapi.json
