# PDF to Text & Markdown – Bulk PDF Extractor with OCR (`gazidev/pdf-text-extractor`) Actor

Extract text or clean Markdown (headings, lists, tables) from PDF URLs in bulk, or from every PDF linked on a web page. Metadata, page ranges, per-page or per-document output, OCR for scanned pages, RAG chunks. Full .md/.txt files saved to the key-value store.

- **URL**: https://apify.com/gazidev/pdf-text-extractor.md
- **Developed by:** [Cemal Atakli](https://apify.com/gazidev) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 pdf page extracteds

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 Text & Markdown – Bulk PDF Extractor with OCR

Convert **PDF to Markdown or plain text in bulk**: give a list of PDF URLs, or a web page whose PDFs you want, and you get back clean, LLM-ready text with **headings, lists and Markdown tables**, plus the PDF **metadata** (title, author, page count, creation date). **Scanned PDFs are read with OCR** (Tesseract, 21 languages), and only the pages that need it are OCR'd. You can split output **per document, per page or into RAG chunks**, and the full `.md` / `.txt` file of every PDF is saved for download.

It downloads over plain HTTP, with no browser and no proxy, so it is fast and cheap: **$0.0005 per page** ($0.50 per 1,000 pages) and $0.005 per OCR page.

### What it does

- **PDF to Markdown.** Headings are detected from font size and weight (`#`, `##`, numbered sections such as `3.1` → `###`). Bullets and numbered lists are kept, paragraphs are re-flowed across line breaks, and hyphenated line breaks are joined (`repre-sentation` → `representation`, while `end-to-end` keeps its hyphens). Ruled tables become **Markdown tables**.
- **PDF to plain text.** The text keeps its original line breaks and has a blank line between paragraphs. This is the fastest mode.
- **Correct reading order for multi-column layouts** (scientific papers, newsletters), using Google's PDFium engine.
- **Clean output.** Running headers, footers and page numbers ("Form W-9 (Rev. 3-2024) Page 2", "Confidential", "3 of 10") are removed automatically.
- **OCR fallback for scanned pages.** Pages with no usable text layer (scans, photos, faxes, broken font encodings) are rendered at 300 DPI and read with Tesseract. Mixed PDFs work page by page. OCR can be set to `auto`, `off` or `force`.
- **Metadata:** `title`, `author`, `subject`, `keywords`, `creator`, `producer`, `createdAt`, `modifiedAt` (ISO 8601), `pageCount`, `pdfVersion`, `encrypted` and `fileSizeBytes`.
- **Page ranges:** `1-5`, `1,3,10-`, or `-3--1` for the last three pages. You only pay for the pages you extract.
- **Bulk input and PDF discovery.** Paste hundreds of PDF URLs, or add web pages (report archives, IR pages, government form lists, documentation) and every linked `.pdf` is extracted, with a `maxPdfs` cap, a same-domain filter and a link regex.
- **Share links work.** Google Drive, Dropbox and GitHub `blob` links and arXiv `/abs/` pages are converted to direct downloads.
- **RAG chunking.** `chunkSize` / `chunkOverlap` (in characters). Chunks are cut at paragraph, then sentence, then word boundaries, start a new chunk at section headings, and carry `pageStart`, `pageEnd`, the `section` heading and a stable `chunkId`.
- **Full files saved.** Every PDF's complete Markdown or text is stored in the run's key-value store, and each row links to it (`contentFileUrl`).
- **Robust.** Each failed PDF becomes a free error row with a clear `errorCode`, and the run carries on. Codes include `http-404`, `not-a-pdf`, `too-large`, `password-required`, `wrong-password`, `invalid-pdf` and `no-pdf-links`. Downloads are retried with backoff on timeouts, 429 and 5xx (honouring `Retry-After`). There is a size limit, and PDFs that only restrict printing or copying open normally. Password-protected PDFs open with `pdfPassword`.

### Use cases

- **RAG and LLM pipelines:** turn PDF libraries (papers, manuals, policies, contracts, filings) into Markdown chunks with page numbers for your vector database (Pinecone, Weaviate, Qdrant, pgvector), LangChain or LlamaIndex.
- **AI agents:** let an agent read any PDF URL as Markdown in one tool call (see below).
- **Research:** bulk-extract arXiv, PubMed or SSRN papers with their metadata.
- **Finance and compliance:** annual reports, 10-Ks, ESG reports and investor presentations from IR pages.
- **Government and legal:** forms, regulations, court documents and tenders. Scanned archives become searchable text through OCR.
- **Digitising scanned documents:** old letters, typewritten reports and archive.org scans.
- **Search indexing and content migration:** PDF knowledge bases to text for Elasticsearch, Algolia or a CMS.

### Input example

```json
{
  "pdfUrls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "pageUrls": ["https://www.irs.gov/forms-instructions"],
  "maxPdfs": 20,
  "outputFormat": "markdown",
  "outputMode": "document",
  "pageRange": "",
  "ocrMode": "auto",
  "ocrLanguages": ["eng"],
  "chunkSize": 1500,
  "chunkOverlap": 200
}
```

The default input (the "Attention Is All You Need" paper and the IRS W-9 form, 21 pages) finishes in about 10 seconds and costs about $0.01.

| Field | Default | Notes |
|---|---|---|
| `pdfUrls` | 2 sample PDFs | Direct PDF links. Drive, Dropbox, GitHub and arXiv links are fixed automatically |
| `pageUrls` | – | Web pages to scan for PDF links (static HTML) |
| `maxPdfs` / `sameDomainOnly` / `pdfLinkRegex` | 50 / false / – | Limits and filters for discovered PDFs |
| `outputFormat` | `markdown` | `markdown` or `text` |
| `outputMode` | `document` | `document`, `page` or `chunk` (one dataset row each) |
| `pageRange` / `maxPagesPerPdf` | all / 500 | `1-5, 8, 10-`, `-3--1` |
| `removeHeadersFooters` | true | Drops running headers, footers and page numbers |
| `extractTables` | true | Ruled tables to Markdown tables |
| `pageMarkers` | false | `<!-- page N -->` before each page |
| `ocrMode` / `ocrLanguages` / `ocrDpi` | auto / eng / 300 | 21 languages: English, German, French, Spanish, Italian, Portuguese, Dutch, Turkish, Polish, Russian, Ukrainian, Czech, Swedish, Romanian, Arabic, Hindi, Chinese, Japanese, Korean, Indonesian and Vietnamese |
| `chunkSize` / `chunkOverlap` | 0 / 200 | Characters. Chunk mode defaults to 1,500 |
| `maxCharsPerItem` | 100,000 | Dataset field limit. The full text is always in the saved file |
| `pdfPassword` | – | For encrypted PDFs (secret field) |
| `maxFileSizeMb` | 50 | Larger files become a free error row |

### Output example

One row per PDF (`document` mode). The Output tab has **PDF documents**, **Pages**, **RAG chunks** and **Errors** views, plus links to the saved files and the run summary.

```json
{
  "type": "document",
  "status": "ok",
  "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
  "fileName": "fw9.pdf",
  "title": "Form W-9 (Rev. March 2024)",
  "author": "SE:W:CAR:MP",
  "subject": "Request for Taxpayer Identification Number and Certification",
  "createdAt": "2024-03-06T13:18:13Z",
  "pdfVersion": "1.7",
  "encrypted": false,
  "pageCount": 6,
  "pagesExtracted": 6,
  "textPages": 6,
  "ocrPages": 0,
  "wordCount": 6475,
  "format": "markdown",
  "content": "Form W-9 (Rev. March 2024)\n\n### Request for Taxpayer Identification Number and Certification\n\n…\n\n| IF the entity/individual on line 1 is a(n) . . . | THEN check the box for . . . |\n| --- | --- |\n| • Corporation | Corporation. |\n…",
  "contentTruncated": false,
  "contentFileUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/0002-fw9.md",
  "extractedAt": "2026-10-04T09:30:58Z"
}
```

- **`page` mode:** one row per page with `pageNumber`, `method` (`text`, `ocr`, `empty`, `needs-ocr` or `failed`), `content`, `wordCount` and `tables`.
- **`chunk` mode:** one row per chunk with `chunkId`, `chunkIndex`, `chunkCount`, `pageStart`, `pageEnd`, `section` and `content`, ready to embed. In `document` mode with `chunkSize` set, the chunks are in a `chunks` array on the document row.
- **Errors:** `{"type": "error", "url": …, "errorCode": "password-required", "error": "The PDF is password-protected. Set \"PDF password\" to open it."}`. Error rows are free.
- **`OUTPUT` record:** the run summary (pages charged, OCR pages, scanned pages skipped, errors).

See `SAMPLE_OUTPUT.json` for real rows: an arXiv paper, the IRS W-9 with a Markdown table, a scanned 1914 letter read by OCR, a 404 error and two RAG chunks.

### Pricing

Pay per event. You pay only for pages actually extracted.

| Event | Price |
|---|---|
| Page extracted (text layer, Markdown or text) | **$0.0005** ($0.50 / 1,000 pages) |
| Scanned page read with OCR (instead of the above) | **$0.005** ($5 / 1,000 pages) |
| Actor start | $0.00005 |

Free: empty pages, scanned pages when OCR is off, pages outside your page range, failed pages, failed downloads, encrypted PDFs without a password, chunks, metadata and saved files.

Compared with other Store Actors (public Store pages, 2026-10-04):

| | **This Actor** | memo23/pdf-text-extractor | automation-lab/pdf-text-extractor |
|---|---|---|---|
| Price model | **$0.0005 / page** | $0.005 / PDF | $0.003 / PDF + $0.005 / run |
| OCR for scanned pages | **$0.005 / page**, 21 languages | $0.015 / page | no OCR |
| Markdown (headings, lists) | yes | yes | no |
| Markdown tables | **yes (ruled tables)** | no (tables as lines) | no |
| RAG chunks with page numbers | **yes** | no | no |
| Find PDFs linked on a web page | **yes** | no | no |
| Page ranges, metadata, encrypted PDFs | yes | yes | metadata |
| 1-page / 10-page / 50-page PDF | **$0.0005** / $0.005 / $0.025 | $0.005 / $0.005 / $0.005 | $0.003 each |
| 10 scanned pages | **$0.05** | $0.155 | – |

The Actor is cheapest for short documents and for any scanned content. For very long text-only PDFs, use `pageRange` to pay only for the pages you need.

Set **Maximum cost per run** on the run options to cap spending. Before each page, the Actor checks that it can still pay for that page. When the budget runs out, it saves what it has, marks the PDF `partial` and stops cleanly with the status "Stopped early: maximum total charge for this run reached".

### FAQ

**How good is the Markdown?**
It is very good for digital PDFs: papers, reports, forms, manuals and contracts. Headings come from font size and boldness, and tables come from drawn table rules. Tables without ruling lines (common in academic papers) stay as text lines, and math formulas come out as plain text. For the best LLM results, keep `removeHeadersFooters` on.

**How do I know if a PDF was scanned?**
`ocrPages` counts the pages read with OCR. In `page` mode, `method` is `ocr` for each such page. With OCR off, scanned pages are reported as `needs-ocr` (`scannedPagesSkipped`) and not charged.

**How accurate is OCR?**
Tesseract 5 is good on clean printed scans at 300 DPI. Old typewriter text, handwriting and low-resolution faxes give noisy output (see the 1914 letter in `SAMPLE_OUTPUT.json`). Choose the right `ocrLanguages`, and use `ocrDpi: 400` for very small print.

**Can it read password-protected PDFs?**
PDFs that only block printing or copying open normally (`encrypted: true`). PDFs that need a password to open require `pdfPassword`. Without one, you get a free `password-required` error row.

**Can I upload files instead of URLs?**
Use any public URL: S3, Google Drive, Dropbox, GitHub, or a record in an Apify key-value store.

**Is there a size limit?**
`maxFileSizeMb` defaults to 50 MB and can go up to 500 MB. `maxPagesPerPdf` defaults to 500. Memory use stays low (about 200 MB for the default run) because pages are processed one at a time. Use 2–4 GB of memory for faster OCR.

**Which libraries does it use? Any AGPL?**
No AGPL. It uses PDFium via `pypdfium2` (Apache-2.0/BSD-3) for text, layout, metadata and rendering, `pdfplumber` (MIT) for table detection and Tesseract (Apache-2.0) for OCR. PyMuPDF / pymupdf4llm (AGPL) are deliberately not used.

### Use with AI agents / Apify MCP

- **Apify MCP server:** add `gazidev/pdf-text-extractor` to Claude Desktop, Cursor, VS Code or any MCP client with `https://mcp.apify.com?actors=gazidev/pdf-text-extractor`. An agent can then call it with `{"pdfUrls":["https://example.com/report.pdf"],"pageRange":"1-10"}` and read the Markdown directly, scanned pages included.
- **API (synchronous):** `POST https://api.apify.com/v2/acts/gazidev~pdf-text-extractor/run-sync-get-dataset-items?token=…` with the input JSON returns the rows in the same HTTP call. This is ideal as a "read PDF" tool for LangChain, LlamaIndex, CrewAI, n8n, Make or Zapier.
- **RAG ingestion:** `"outputMode": "chunk", "chunkSize": 1500, "chunkOverlap": 200` returns one row per chunk with `chunkId`, `pageStart`/`pageEnd` and `section`, ready to embed. Use the Apify Pinecone, Qdrant or Weaviate integrations to load them directly.
- **Scheduled monitoring:** a scheduled Task with `pageUrls` pointing at a publications page extracts newly posted PDFs every week.

### More from the toolkit

- [Website to Markdown](https://apify.com/gazidev/website-to-markdown): any URL, sitemap or whole website to clean Markdown with RAG chunks and llms.txt. It is the HTML counterpart of this Actor.
- [Document & Invoice to JSON](https://apify.com/gazidev/document-invoice-to-json): invoices, receipts and DOCX/PDF documents to structured JSON fields (totals, VAT, line items).
- [Wayback Machine URL Extractor](https://apify.com/gazidev/wayback-url-extractor): find old PDFs a site used to host (`mimeTypes: application/pdf`), then extract them here.
- [Sitemap Extractor](https://apify.com/gazidev/sitemap-url-extractor): list every URL, including PDFs, from a site's sitemaps.

# Actor input Schema

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

Direct links to PDF files, one per line. Google Drive / Dropbox share links, GitHub `blob` links and arXiv `/abs/` pages are converted to direct downloads automatically.

## `pageUrls` (type: `array`):

Optional. The Actor loads each page (plain HTTP, no browser) and extracts every PDF it links to (links ending in `.pdf`, or matching `PDF link regex`). Great for report archives, documentation, investor-relations or government form pages.

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

Stop after this many PDFs in total (direct URLs + PDFs found on pages). 0 = no limit.

## `sameDomainOnly` (type: `boolean`):

When scanning web pages, skip PDF links that point to other domains.

## `pdfLinkRegex` (type: `string`):

Optional. When set, a link found on a scanned page is taken if its URL matches this regex (case-insensitive), even without a `.pdf` extension, e.g. `/download/|annual-report`.

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

`markdown`: headings (`#`), lists, ruled tables as Markdown tables, paragraphs re-flowed - best for LLMs and RAG. `text`: plain text with the original line breaks, faster.

## `outputMode` (type: `string`):

`document`: one row per PDF with metadata and the full content. `page`: one row per page. `chunk`: one row per RAG chunk (uses `Chunk size`, default 1,500 characters). The price is the same in every mode (per page).

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

Optional. Pages to extract, e.g. `1-5`, `1,3,10-`, or `-3--1` for the last three pages. Empty = all pages (up to `Max pages per PDF`). Only extracted pages are charged.

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

Safety cap on pages extracted from one PDF (after the page range). 0 = no limit.

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

Drops lines repeated at the top or bottom of many pages (journal names, 'Confidential', 'Page 3 of 10') and bare page numbers. Needs 3+ pages for repeated lines.

## `extractTables` (type: `boolean`):

Markdown format only: detect ruled tables (with drawn lines) and output them as Markdown tables. Tables without lines stay as text lines.

## `pageMarkers` (type: `boolean`):

Adds `<!-- page N -->` (Markdown) or `--- page N ---` (text) before each page in the document content and saved file.

## `maxCharsPerItem` (type: `integer`):

Longer content is cut in the dataset (`contentTruncated: true`). The complete text is always in the saved .md/.txt file (`contentFileUrl`).

## `saveToKeyValueStore` (type: `boolean`):

Save each PDF's full Markdown or text as a file in the run's key-value store, linked from every row (`contentFileUrl`).

## `ocrMode` (type: `string`):

`auto`: pages without a usable text layer (scans, photos, broken font encodings) are read with Tesseract OCR and charged as `ocr-page`; normal pages are not OCR'd. `off`: never OCR (scanned pages are reported, not charged). `force`: OCR every page.

## `ocrLanguages` (type: `array`):

Languages of the scanned text. Pick 1-3: more languages are slower.

## `ocrDpi` (type: `integer`):

Rendering resolution for OCR. 300 is best for normal print; 200 is a little faster; 400 helps with very small text.

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

Split each PDF into RAG-ready chunks of about this many characters, cut at paragraph, sentence or word boundaries, with page numbers and the section heading. 0 = no chunks (in `chunk` mode the default is 1,500). Chunks are free.

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

Characters of the previous chunk repeated at the start of the next one (at most half the chunk size).

## `pdfPassword` (type: `string`):

Optional. Password for encrypted PDFs (used for all PDFs of the run). PDFs that only restrict printing/copying open without it.

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

Larger files are skipped with an error row (free).

## `requestTimeoutSecs` (type: `integer`):

Timeout for one download. Failed downloads (timeouts, HTTP 429/5xx) are retried 3 times with backoff.

## `maxConcurrentDownloads` (type: `integer`):

PDFs downloaded in parallel while the previous ones are being extracted.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "pageUrls": [],
  "maxPdfs": 50,
  "sameDomainOnly": false,
  "outputFormat": "markdown",
  "outputMode": "document",
  "maxPagesPerPdf": 500,
  "removeHeadersFooters": true,
  "extractTables": true,
  "pageMarkers": false,
  "maxCharsPerItem": 100000,
  "saveToKeyValueStore": true,
  "ocrMode": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "ocrDpi": 300,
  "chunkSize": 0,
  "chunkOverlap": 200,
  "maxFileSizeMb": 50,
  "requestTimeoutSecs": 120,
  "maxConcurrentDownloads": 3
}
```

# Actor output Schema

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

No description

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

No description

## `chunks` (type: `string`):

No description

## `errors` (type: `string`):

No description

## `files` (type: `string`):

No description

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

No description

## `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 = {
    "pdfUrls": [
        "https://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gazidev/pdf-text-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://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("gazidev/pdf-text-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://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call gazidev/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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