# PDF to Markdown (`agarwoods/pdf-to-markdown`) Actor

Turn PDF links and uploads into clean Markdown or plain text for AI use, with headings, lists and simple tables, per document or per page. No browser or paid API. Scanned pages are flagged, with no OCR.

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

## Pricing

from $1.70 / 1,000 pages

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

Turn PDF files into clean Markdown or plain text, ready for AI tools, search and analysis, at $2 per 1,000 pages. Give it links or upload files, and get back headings, paragraphs, lists and simple tables in reading order. You don't need a browser, a login or an API key.

### What you get

- One row per document, or one row per page if you need to cite page numbers
- GitHub-style Markdown, or plain text with table cells separated by tabs
- Headings worked out from font size, plus lists, paragraphs and simple tables worked out from text position
- Multi-column pages read one column at a time
- Title, author, page count, word count and table count
- A clear flag or free status row when a page has no text to read, such as a scan

### Sample output

```json
{
  "recordType": "document",
  "sourceUrl": "https://example.com/annual-report.pdf",
  "fileName": "annual-report.pdf",
  "title": "Annual Report",
  "author": "Jane Writer",
  "pageCount": 1,
  "pagesReturned": 1,
  "pageNumber": null,
  "part": 1,
  "partCount": 1,
  "markdown": "# Annual Report\n\n## Overview\n\nThis paragraph has plenty of ordinary words in it...\n\n## Results\n\n| Region | Sales | Growth |\n| --- | --- | --- |\n| North | 1,200 | 5% |\n| South | 950 | 3% |",
  "text": null,
  "wordCount": 107,
  "tableCount": 1,
  "hasTextLayer": true,
  "noTextLayerPages": [],
  "possiblyGarbled": false,
  "status": null,
  "reason": null,
  "fetchedAt": "2026-11-02T12:48:47.489Z"
}
```

This comes from a small test PDF, with the Markdown shortened here. With the output format set to text, the text field is filled and the markdown field is empty.

### How to use it

1. Add links to PDF files, one per line, or upload files.
2. Choose Markdown or plain text, and one row per document or per page.
3. If you only need part of each file, set a page range, such as 1-5 or 1,3,7-9. Then press Start.

Maximum pages per file defaults to 100, so a longer file needs a higher setting. A status row tells you when a file was cut short. Maximum file size defaults to 50 MB. If you are reading very large files, raise the run's memory too.

### Pricing

You pay only for pages returned, and Apify platform usage is included.

| Event | Price |
| --- | --- |
| Each page returned | $0.002 ($2 per 1,000) |
| Each run started | $0.00005 |

Status rows are free. So are pages with no text layer, pages that could not be read, and files that could not be opened. A 100-page PDF costs about $0.20. If a document is too long for one row and is split, you still pay by the page, not by the row.

If you set a maximum cost for the run, you get only the pages it pays for, and a status row tells you it stopped. A document cut off this way has pagesReturned set to the pages you received.

On the free Apify plan, each run returns up to 100 pages, and a status row tells you when you reach that limit. Any paid Apify plan removes it.

### Status rows

The Actor always tells you what happened. Problems never stop the rest of the run. Each one gives a free row with recordType set to status, and reason says what went wrong in plain words.

| Status | Meaning |
| --- | --- |
| no_input | No links or files were given |
| invalid_input | The page range could not be understood |
| invalid_url | Not a valid http or https link |
| blocked_url | The link points to a private or reserved address, so it was refused |
| not_found | The server has no file at this link |
| access_denied | The server refused access |
| http_error | The server returned another error |
| download_failed | The download could not be completed |
| too_many_redirects | The link redirected too many times |
| too_large | The file is over your maximum file size |
| not_pdf | The link returned something that isn't a PDF |
| encrypted | The PDF needs a password |
| corrupt | The file is damaged and could not be read |
| too_complex | Reading the file took too long or used too much memory |
| no_text_layer | Some or all pages have no text, and noTextLayerPages lists them |
| page_failed | Some pages could not be read, and reason lists them |
| no_pages_selected | The page range matches no pages in this file |
| max_pages | The file has more pages than your maximum pages per file |
| spending_limit | The run reached your maximum cost |
| free_limit | The run reached the free plan limit |

If you only want content, filter out rows with recordType set to status.

### What it can't do

- No OCR. Scanned pages have no text to read, so they are flagged and not charged. A scan with a small stamp of real text counts as text, and you get only the stamp.
- Complex tables may be imperfect. Tables are found by how text lines up, not by their borders. Merged cells, cells with long running text and tables with no clear gaps between columns can come out wrong, or as ordinary text. Small labels in a chart can look like a small table.
- Multi-column pages are worked out from positions, so unusual layouts can read in the wrong order.
- Headings come from font size only, so a bold heading in the same size as the body text stays a paragraph.
- Images and charts are not described, and rotated text goes at the end of the page.
- Page headers, footers and page numbers stay in the text.
- A paragraph that runs over a page break stays as two paragraphs.
- Some PDFs have fonts with no usable letter mapping. When that shows up as symbols, possiblyGarbled is true, but gibberish made of ordinary letters can't be detected.
- Password-protected files can't be read, and there is no input for a password. A file that opens with no password but restricts copying is read as normal, so make sure you have the right to use its content.

### Long documents

Apify limits each dataset row to 9 MB. A longer document or page is split across rows, at page breaks first, then at paragraph breaks. The part and partCount fields show the order, and the other fields repeat on each part. The word and table counts describe the whole document or page, so count them once per document.

### Using it with AI agents

The main inputs are lists, every field is optional, and no credentials are needed. An agent can send many links in one call and read status rows to see what went wrong.

### Data and use

This Actor reads only the files you point it to. It refuses private and internal network addresses, including after a redirect, and it keeps no copy beyond this run's own storage. You are responsible for having the right to process each file. Text is extracted with the open-source PDF.js library. This Actor is not affiliated with or endorsed by Adobe, Mozilla or the publisher of any PDF.

### Support

Open an issue on the Issues tab with your input and what you expected.

# Actor input Schema

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

Links to PDF files, one per line. Only public http and https links work. Leave empty if you upload files below. With nothing to read, the run returns one free status row.

## `uploadedFiles` (type: `array`):

Optional. PDF files to upload from your computer. They are read the same way as links, and the upload must be readable from a public link.

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

markdown fills the markdown field, with headings, lists and tables. text fills the text field, with tables separated by tabs. The other field is left empty.

## `mode` (type: `string`):

document gives one row per PDF. page gives one row per page, which helps when you need to cite page numbers. The price per page is the same.

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

Optional. The pages to read from each file, such as 1-5, 1,3,7-9 or 10-. Leave empty for all pages.

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

Read at most this many pages from each file, after the page range. This also caps the cost per file.

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

Skip any file larger than this, with a free status row. The download stops as soon as it passes the limit. Raise the run's memory for very large files.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "uploadedFiles": [],
  "outputFormat": "markdown",
  "mode": "document",
  "pageRange": "",
  "maxPages": 100,
  "maxFileSizeMb": 50
}
```

# Actor output Schema

## `documents` (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 = {
    "pdfUrls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("agarwoods/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://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("agarwoods/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://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call agarwoods/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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