# PDF to Markdown and Text: Tables, Pages, Metadata for RAG (`jtpalms/pdf-to-markdown`) Actor

PDF parser and text extractor: turn PDF URLs into clean Markdown, plain text, per-page text and detected tables. Fast, no browser, no proxy. Flags scanned pages that need OCR instead of charging for empty text. Built for RAG and document pipelines. USD 2 per 1,000 PDFs (up to 100 pages each).

- **URL**: https://apify.com/jtpalms/pdf-to-markdown.md
- **Developed by:** [JT Palms](https://apify.com/jtpalms) (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.60 / 1,000 pdf extracteds

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 and Text: Tables, Pages, Metadata for RAG

Give it PDF URLs, get one row per PDF with clean Markdown, plain text, optional per-page text and detected tables, plus metadata and an honest flag for pages that are scanned images. It runs on pure JavaScript: no browser, no proxy, no LLM, no OCR. Reading takes roughly 10 to 60 milliseconds per page depending on the machine.

**USD 2 per 1,000 PDFs** (up to 100 pages each), plus a tiny start fee per run. Failed downloads, non-PDF answers and PDFs without a text layer are free.

### What people use it for

- **RAG and document pipelines.** Reports, manuals, policies and filings become Markdown with real headings, paragraphs and lists, ready to chunk and embed. Turn on `outputPages` to chunk by page, and keep `headings` and `title` as chunk metadata.
- **Filings and statistics.** Regulator and statistics-agency PDFs often hold their numbers in tables. With `outputTables` on, each table comes back as row arrays (for code) and as a Markdown table (for LLMs), with a confidence rating.
- **Tender and procurement documents.** Pair it with [Government Tenders Monitor](https://apify.com/JTPalms/gov-tenders-monitor): that actor returns `documentsUrl` and `noticeUrl` for each notice, and when those links point straight at a PDF, this actor turns the document into text you can search or summarise. Schedule both.
- **Price lists and supplier documents.** Pull the text and tables out of a supplier's PDF price list every week and compare.
- **An agent tool.** "Read this PDF" as one cheap, deterministic call that returns structured JSON.

### Sample output

Real run on the IRS Form W-9 (`https://www.irs.gov/pub/irs-pdf/fw9.pdf`), default settings, `markdown` shortened:

```json
{
  "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "finalUrl": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "httpStatus": 200,
  "contentType": "application/pdf",
  "fileSizeBytes": 140815,
  "pageCount": 6,
  "pagesProcessed": 6,
  "title": "Form W-9 (Rev. March 2024)",
  "titleSource": "metadata",
  "subject": "Request for Taxpayer Identification Number and Certification",
  "keywords": "Fillable",
  "creatorTool": "Designer 6.5",
  "producer": "Designer 6.5",
  "createdAt": "2024-03-06T13:18:13.000Z",
  "modifiedAt": "2024-03-06T13:18:13.000Z",
  "language": "en-US",
  "isEncrypted": false,
  "markdown": "Form W-9\n\n# Request for Taxpayer Identification Number and Certification\n\n...",
  "wordCount": 6237,
  "charCount": 37706,
  "headings": [
    { "level": 1, "text": "Request for Taxpayer Identification Number and Certification", "page": 1 },
    { "level": 3, "text": "Part I Taxpayer Identification Number (TIN)", "page": 1 }
  ],
  "links": [],
  "ocrNeededPages": [],
  "truncated": false,
  "notes": [
    "4 text item(s) at an angle (watermarks, side stamps) were left out.",
    "11 repeated header, footer or page-number line(s) removed."
  ],
  "error": null,
  "errorCode": null,
  "fetchedAt": "2026-10-06T19:27:29.851Z"
}
```

A table from the same run mode with `outputTables` on (Table A-8 of a Bureau of Labor Statistics release, first rows, as it appears in the Markdown):

```
| Category | Sept. | Aug. | Sept. | Sept. | May | June | July | Aug. | Sept. |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|  | 2025 | 2026 | 2026 | 2025 | 2026 | 2026 | 2026 | 2026 | 2026 |
| CLASS OF WORKER |  |  |  |  |  |  |  |  |  |
| Agriculture and related industries | 2,209 | 2,551 | 2,636 | 2,206 | 2,261 | 2,291 | 2,216 | 2,444 | 2,596 |
| Wage and salary workers1 | 1,533 | 1,735 | 1,937 | 1,518 | 1,648 | 1,595 | 1,508 | 1,630 | 1,832 |
```

In the dataset the same table is `tables[]`: `{ "page": 2, "columns": 10, "rowCount": 26, "confidence": "high", "rows": [["Category", "Sept.", ...], ...] }`. Dot leaders ("Revenue . . . . 1,200") are removed and the value after them is its own cell.

### Input

| Field | What it does |
|---|---|
| `urls` | PDF links, one per line (public http or https). Duplicates are read once. The default list is three small public PDFs from US government sites. |
| `outputFormat` | `markdown` (default), `text`, or `both`. |
| `outputPages` | Add `pages[]`: for every page read, its text (and Markdown unless the format is `text`), character count and `needsOcr`. |
| `outputTables` | Detect tables and add `tables[]` and pipe tables in the Markdown. |
| `removeHeadersFooters` | On by default. Drops repeated page headers and footers (text that repeats on more than 60% of the pages, also when odd and even pages differ) and bare page numbers. Needs 3 or more pages to spot repeats. |
| `pageRange` | For example `1-5,9` or `10-`. The same range is applied to every PDF. Empty reads everything up to `maxPages`. |
| `maxPages` | Pages read per PDF: default 300, up to 2,000 (the prefill is 50). More pages than that set `truncated: true`. |
| `maxFileMb` | Larger files are skipped and not charged. Default 50, up to 200. |
| `password` | For encrypted PDFs you are entitled to open. Used only to open the file, never output. |
| `includeAuthor` | Off by default. Adds `author`, the PDF's Author field (which can be a person's name). |
| `maxConcurrency` | PDFs in parallel, 1 to 10 (default 4). Requests to one site are still at least one second apart. |
| `timeoutSecs` | Per file: time to download it, and again to read it. At the limit you get the pages read so far with `truncated: true`. |

### Output fields

| Field | Meaning |
|---|---|
| `url`, `finalUrl`, `httpStatus`, `contentType`, `fileSizeBytes` | The request and the answer. |
| `pageCount`, `pagesProcessed` | Pages in the PDF, and pages read. |
| `title`, `titleSource` | The PDF's Title metadata (`metadata`). If it is missing or looks like a file name ("Microsoft Word - report.docx"), the first top-level heading is used (`heading`). |
| `subject`, `keywords`, `creatorTool`, `producer`, `createdAt`, `modifiedAt`, `language` | PDF metadata, or `null`. Dates are ISO 8601. |
| `author` | Only with `includeAuthor`. |
| `isEncrypted` | The file is encrypted (some files are marked encrypted but open without a password, as in any PDF viewer). |
| `markdown`, `text` | The document, as requested by `outputFormat`. |
| `wordCount`, `charCount` | Size of the extracted text. |
| `headings` | `[{ level, text, page }]`. |
| `links` | Web links (http and https) found in the PDF's link annotations. Email and phone links are not output. |
| `pages` | With `outputPages`. |
| `tables` | With `outputTables`: `[{ page, columns, rowCount, confidence, rows }]`. |
| `ocrNeededPages` | Pages with almost no text (under 20 characters) that contain images: scans. |
| `truncated` | `true` when pages were left unread: `maxPages`, the time limit, or your maximum cost per run. |
| `notes` | Plain-language remarks (pages cut, lines removed, scanned pages). |
| `error`, `errorCode` | Why a row has no content, otherwise `null`. See below. |
| `fetchedAt` | When the file was fetched. |

The run's key-value store has a `SUMMARY` record with counts (rows, failures by code, PDFs without text, billed PDFs and extra pages) and request statistics.

#### Errors are rows, not crashes

| `error` | `errorCode` | Charged |
|---|---|---|
| `not a PDF` (an HTML page that answers 200, an empty body) | `NOT_PDF` | no |
| `HTTP 404` and other HTTP errors, `HTTP 403 (access denied)`, `HTTP 429 (rate limited)` | `HTTP_ERROR`, `ACCESS_DENIED`, `RATE_LIMITED` | no |
| `blocked by robots.txt`, or robots.txt could not be read | `ROBOTS_BLOCKED`, `ROBOTS_UNREADABLE` | no |
| `file too large (...)` | `TOO_LARGE` | no |
| `password required`, `wrong password` | `PASSWORD_REQUIRED`, `WRONG_PASSWORD` | no |
| `no text found: the pages are scanned images and need OCR...` or `no extractable text` | `NO_TEXT` | no |
| `could not parse PDF: ...` | `PARSE_FAILED` | no |
| `pageRange selects no pages (the PDF has N)` | `BAD_PAGE_RANGE` | no |
| `timeout after N s`, `domain not found`, `TLS error`, `connection refused`, `private or internal address, not allowed`, bad redirects | `TIMEOUT`, `DNS`, `TLS`, `CONNECTION_REFUSED`, `PRIVATE_ADDRESS`, `BAD_REDIRECT`, `FETCH_FAILED` (and `INTERNAL` for anything unexpected) | no |
| `invalid URL`, `unsupported URL: only http and https are read` | `INVALID_URL` | no |

### Scanned PDFs

This actor does not do OCR. A page with fewer than 20 characters of text that contains images is listed in `ocrNeededPages` (and has `needsOcr: true` in `pages[]`). A PDF is charged only when at least one page has 20 or more characters of text, so a pure scan is free and a mixed document is charged once, with its scanned pages flagged. Run OCR on the flagged pages with a tool of your choice.

### What it does not do

- **No OCR** (v1). Scanned pages come back empty and flagged, never guessed.
- **No form field values, no images, no figures.** Only the page text. Text inside charts and figures is read as it is stored and can look odd.
- **Tables are a heuristic.** Rows of text that line up in 3 or more columns become a table. Simple tables work well (the sample above is 10 columns by 26 rows, `confidence: "high"`), complex layouts with merged cells or wrapped cell text may not. `confidence` is `high`, `medium` or `low`: low means ragged columns, so check the result. Tables that continue over a page break are returned as one table per page.
- **Headings come from font size.** Lines noticeably larger than the document's body text become headings (levels 1 to 4, largest first). A heading in bold at body size stays a paragraph, and a bold run-in heading is not split from its paragraph.
- **Complex layouts can read out of order.** Pages with 2 to 4 plain text columns are read column by column; pages that mix boxes, sidebars, forms and columns (a fillable form is the usual example) may come out in a different order than a person would read them.
- **Hyphens at line ends.** A hyphen at the end of a line followed by a lower-case letter is treated as hyphenation and removed ("interna-" and "tional" become "international"). A real compound broken over two lines ("English-" and "to-German") loses its hyphen. A hyphen followed by a capital or a digit is kept ("COVID-" and "19" become "COVID-19").
- **Rotated text.** A page drawn sideways is turned upright. Text at other angles (watermarks, side stamps) is left out and noted.
- **Fonts without a text mapping.** Some PDFs store text in fonts with no Unicode mapping; their text comes out as odd characters or as nothing, exactly as copy and paste from a viewer would.

### Pricing

| What | Price |
|---|---|
| One PDF with extracted text (up to 100 pages read) | USD 0.002 (USD 2 per 1,000) |
| Each page read beyond 100 in one PDF | USD 0.0001 |
| Run start | USD 0.001 per run |
| Failed downloads, non-PDF answers, errors, PDFs with no text layer | Free |

Worked examples on the free plan:

- The default input (3 small PDFs): 3 x 0.002 + 0.001 = **USD 0.007**.
- 1,000 PDFs of 20 pages: **USD 2.00** (+ the start fee).
- One 300-page report: 0.002 + 200 x 0.0001 = **USD 0.022**.
- 10,000 PDFs of which 800 fail or are scans: 9,200 x 0.002 = **USD 18.40**.

Higher Apify plans get a discount (10% on Silver, 20% on Gold and above). You can set a maximum cost per run: the actor reads no more PDFs than that budget can pay for, reads fewer pages of a long PDF when the extra pages would not fit (the row says `truncated: true`), stops cleanly and keeps every row already written.

### Speed and limits

- Reading time is about 10 milliseconds per page on a laptop and roughly 50 milliseconds per page on the default 1 GB platform container (a 120-page, three-column IRS publication takes about 7 seconds there, a 6-page form well under a second), plus download time. The default three-PDF run finishes in about 4 seconds.
- Requests to one website are at least one second apart, so a list of 1,000 PDFs that are all on the same site takes at least 17 minutes however many you read in parallel. Lists spread over many sites run in parallel.
- Memory: the default is 1 GB. Several large files are held in memory at once, so with a large `maxFileMb` fewer PDFs are read in parallel. A row must fit in one Apify dataset item (under 9 MB): a row that would be larger loses `pages[]` first and then has its text cut at a paragraph, with a note.

### Schedule it

Save your list as a task and schedule it: new filings, price lists or tender documents on the same URLs are read again on every run. Combine it with [Government Tenders Monitor](https://apify.com/JTPalms/gov-tenders-monitor) (daily bid alerts with links to documents) or [RSS Feed Reader and Monitor](https://apify.com/JTPalms/rss-feed-monitor) (feeds that link to PDF reports): feed the document links into the `urls` field of this actor.

### Related actors

- [Government Tenders Monitor: Canada, UK & EU Bid Alerts](https://apify.com/JTPalms/gov-tenders-monitor): New public tenders from official open data in one clean feed: CanadaBuys (federal Canada), UK Find a Tender, EU TED, and US SAM.gov with...
- [RSS Feed Reader & Scraper: Atom, JSON Feed & New-Item Monitor](https://apify.com/JTPalms/rss-feed-monitor): RSS feed reader and scraper.
- [Website to Markdown Crawler for RAG: Fast, No Browser](https://apify.com/JTPalms/website-to-markdown-crawler): Crawl a site into clean Markdown for RAG and LLMs without a browser.
- [News Article Extractor: Text, Date, Image from URLs and Feeds](https://apify.com/JTPalms/news-article-extractor): News article scraper and extractor: turn article URLs, RSS or Atom feeds and news sitemaps into clean rows: title, Markdown and plain...

### Rights, robots.txt and privacy

- **You are responsible for having the right to process a document.** This actor reads files at addresses you supply. It does not crawl, log in, get past paywalls or bot protection, or use proxies.
- **robots.txt is respected** for the host of every file (cached per site, also after redirects). A site that disallows automated access gets a `blocked by robots.txt` row. If robots.txt cannot be read at all (a server error), the file is skipped with a clear reason instead of being fetched blind.
- The User-Agent is `Mozilla/5.0 (compatible; pdf-to-markdown/1.0; +https://apify.com/JTPalms)`. Requests to one host are at least one second apart, and transient errors are retried twice with back-off.
- Private and internal addresses (localhost, 10.x, 192.168.x and similar) are refused.
- **Author names are dropped by default.** The PDF Author field can be a person's name, so it is output only with `includeAuthor`. The actor returns what the document itself contains and nothing else: no enrichment, no lookups. Email and phone links in annotations are not listed in `links`.
- A password you give is used to open that file only. It is not written to the dataset, the summary or the log.
- Encrypted files that need a password are read only with the password you provide; the actor never tries to guess or remove passwords.

### FAQ

**Why was my PDF not charged?** Failed downloads, non-PDF answers, files over the size limit, password problems and PDFs without any text layer are free. The row explains why in `error`.

**Can it read scanned PDFs?** Not in this version. It tells you which pages need OCR and does not charge for empty text.

**How do I get text per page?** Set `outputPages` to true. `pages[]` has one entry per page read with its text and Markdown.

**Can I read only part of a long PDF?** Yes: `pageRange` (for example `1-20`) or `maxPages`. Pages beyond 100 cost USD 0.0001 each, so a range keeps the price down.

**A table came out wrong.** Check `confidence`. Table detection is a heuristic; open an issue on the actor page with the PDF URL and the page number.

# Actor input Schema

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

Links to PDF files, one per line. Each must be a public http or https address of a PDF. Duplicates are read once. The default list is three small public PDFs from US government sites.

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

markdown: headings, paragraphs, lists (and tables when switched on). text: plain text with the same reading order. both: both fields.

## `outputPages` (type: `boolean`):

Add pages\[] with the text of every page read (and its Markdown, unless the format is plain text). Useful when you chunk by page.

## `outputTables` (type: `boolean`):

Find tables (rows of text that line up in 3 or more columns) and add them to tables\[] as row arrays with a confidence, and to the Markdown as pipe tables. A heuristic: simple ruled and unruled tables work well, complex layouts may not.

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

Drop lines at the top or bottom of the page that repeat on more than 60% of the pages, and bare page numbers. Needs at least 3 pages to spot repeats.

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

Pages to read, for example 1-5,9 or 10- (page 10 to the end). Empty means all pages, up to the page limit. The same range is applied to every PDF.

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

Pages read per PDF (1 to 2,000). When a PDF has more, the row says truncated: true. The first 100 pages are covered by the PDF price; each page beyond 100 is one extra page.

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

PDFs larger than this are skipped with an error row and are free. 1 to 200 MB.

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

Password for encrypted PDFs that you are entitled to open. Used only to open files, never stored or output. Without it, a PDF that asks for a password returns an error row.

## `includeAuthor` (type: `boolean`):

Adds author (the PDF's Author field, which can be a person's name). Off by default.

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

How many PDFs are downloaded and read at the same time (1 to 10). Requests to one site are still spaced at least one second apart.

## `timeoutSecs` (type: `integer`):

Time allowed to download one file, and again to read it. A PDF still being read at the limit returns the pages read so far with truncated: true.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    },
    {
      "url": "https://www.federalreserve.gov/monetarypolicy/files/monetary20240131a1.pdf"
    },
    {
      "url": "https://www.govinfo.gov/content/pkg/CFR-2012-title12-vol1/pdf/CFR-2012-title12-vol1-part1.pdf"
    }
  ],
  "outputFormat": "markdown",
  "outputPages": false,
  "outputTables": false,
  "removeHeadersFooters": true,
  "maxPages": 50,
  "maxFileMb": 50,
  "includeAuthor": false,
  "maxConcurrency": 4,
  "timeoutSecs": 60
}
```

# Actor output Schema

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

One row per PDF in the default dataset (JSON, CSV, Excel via the format parameter).

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

Counts (rows, failures, PDFs without text, billed PDFs and extra pages) and request statistics.

# 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://www.irs.gov/pub/irs-pdf/fw9.pdf"
        },
        {
            "url": "https://www.federalreserve.gov/monetarypolicy/files/monetary20240131a1.pdf"
        },
        {
            "url": "https://www.govinfo.gov/content/pkg/CFR-2012-title12-vol1/pdf/CFR-2012-title12-vol1-part1.pdf"
        }
    ],
    "maxPages": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("jtpalms/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://www.irs.gov/pub/irs-pdf/fw9.pdf" },
        { "url": "https://www.federalreserve.gov/monetarypolicy/files/monetary20240131a1.pdf" },
        { "url": "https://www.govinfo.gov/content/pkg/CFR-2012-title12-vol1/pdf/CFR-2012-title12-vol1-part1.pdf" },
    ],
    "maxPages": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("jtpalms/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://www.irs.gov/pub/irs-pdf/fw9.pdf"
    },
    {
      "url": "https://www.federalreserve.gov/monetarypolicy/files/monetary20240131a1.pdf"
    },
    {
      "url": "https://www.govinfo.gov/content/pkg/CFR-2012-title12-vol1/pdf/CFR-2012-title12-vol1-part1.pdf"
    }
  ],
  "maxPages": 50
}' |
apify call jtpalms/pdf-to-markdown --silent --output-dataset

```

## MCP server setup

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