# PDF to Text & Markdown Extractor for RAG (`impressionable_lupine/pdf-text-extractor`) Actor

Convert PDFs from URLs or a dataset to clean text or Markdown with tables, metadata and RAG-ready chunks (page ranges + token estimates). Optional OCR for scanned pages. $0.003 per PDF; failed or password-protected PDFs are free.

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

## Pricing

from $3.00 / 1,000 pdf processeds

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 Extractor — tables and RAG chunks, bulk URLs

**Convert PDF to text or PDF to Markdown in bulk.** Paste PDF URLs (or point the Actor at a dataset of URLs) and get one clean JSON item per PDF: full **Markdown or plain text**, **tables** as rows and Markdown, **metadata** (title, author, dates, page count) and **RAG-ready chunks** with page ranges and token estimates. Scanned pages are detected and can be read with built-in **OCR**.

**$0.003 per processed PDF** ($3 per 1,000). Password-protected, scanned-without-OCR, broken, oversized or unreachable PDFs are **free**. Platform usage is included.

Use it as a **PDF parser API** for LLM agents, retrieval-augmented generation (RAG), search indexing, document analytics, or anywhere you need to **extract text from PDF** files at scale.

### Why this PDF extractor

- **Markdown that LLMs read well** — headings (`#`, `##`, `###`) from font sizes and numbered sections, bullet and numbered lists, paragraphs reflowed across line breaks, soft hyphens removed ("hyphen-ation" → "hyphenation") while real compounds stay ("task-specific").
- **Tables** — detected from column alignment and returned as `rows[][]` *and* a Markdown table. Turn off with `extractTables: false`.
- **RAG chunks built in** — `chunks[]` follow heading, paragraph and sentence boundaries, with `pageStart`/`pageEnd`, `tokensEstimate`, `section` and a stable `id`. Configure `chunkSize` and `chunkOverlap`.
- **Clean text for retrieval** — running headers, footers and page numbers are removed; two-column papers keep their reading order; out-of-order link text is re-inserted where it belongs.
- **Bulk input** — up to 10,000 URLs per run, or read URLs from any Apify dataset (`datasetId` + `datasetUrlField`, dot paths and arrays supported). Duplicates removed.
- **Friendly links** — arXiv `/abs/` pages, Google Drive share links, Dropbox `?dl=0` and GitHub `blob` links are converted to direct downloads.
- **Clear statuses, honest billing** — every PDF gets a `status` and `statusMessage`; you only pay for `success` and `partial`.
- **Permissive open-source stack** — Mozilla PDF.js (Apache-2.0) and Tesseract (Apache-2.0). No AGPL/GPL components. See [LICENSES.md](LICENSES.md).

### Quick start

1. Click **Try for free**. The input is prefilled with the "Attention Is All You Need" paper on arXiv.
2. Add your own PDF URLs (one per line) or a dataset ID.
3. Choose **Markdown** or **Plain text**, then **Start**.
4. Open the **Overview**, **Content**, **RAG chunks**, **Tables** or **Not charged / failures** tabs, or download JSON, CSV, Excel.

#### Example input

```json
{
  "pdfUrls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputFormat": "markdown",
  "extractTables": true,
  "chunkSize": 800,
  "chunkOverlap": 100,
  "maxPages": 200
}
```

#### Read URLs from another Actor's dataset

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "datasetUrlField": "pdfUrl",
  "outputFormat": "text",
  "chunkSize": 500
}
```

#### Example output (one item per PDF, shortened)

```json
{
  "url": "https://arxiv.org/pdf/1706.03762",
  "status": "success",
  "statusMessage": "All pages processed.",
  "title": "Attention Is All You Need",
  "author": null,
  "creationDate": "2024-04-10T21:11:43.000Z",
  "pageCount": 15,
  "pagesProcessed": 15,
  "truncated": false,
  "pagesNeedingOcr": [],
  "outputFormat": "markdown",
  "markdown": "# Attention Is All You Need\n\nAshish Vaswani ... \n\n## Abstract\n\nThe dominant sequence transduction models ...",
  "wordCount": 6300,
  "tokensEstimate": 10200,
  "tableCount": 8,
  "tables": [
    {
      "index": 0,
      "page": 6,
      "rowCount": 4,
      "columnCount": 4,
      "rows": [["Self-Attention", "O(n2 · d)", "O(1)", "O(1)"], ["Recurrent", "O(n · d2)", "O(n)", "O(n)"]],
      "markdown": "| Self-Attention | O(n2 · d) | O(1) | O(1) |\n| --- | --- | --- | --- |\n| Recurrent | O(n · d2) | O(n) | O(n) |"
    }
  ],
  "chunkCount": 17,
  "chunks": [
    {
      "id": "3f2a9c1b7e-0",
      "index": 0,
      "text": "# Attention Is All You Need\n\n...",
      "pageStart": 1,
      "pageEnd": 2,
      "tokensEstimate": 790,
      "charCount": 3160,
      "section": "Attention Is All You Need"
    }
  ],
  "charged": true,
  "chargedEvents": { "pdf-processed": 1 },
  "processingMs": 1468
}
```

Numbers above come from a local test run; your values may differ slightly between Actor versions. The full field list is in the dataset schema (`.actor/dataset_schema.json`).

### Pricing

Pay per event — you pay only for results:

| Event | Price | When it is charged |
| --- | --- | --- |
| `pdf-processed` | **$0.003** per PDF | Once per PDF with status `success` or `partial` (text was extracted). Any page count up to `maxPages`. |
| `ocr-page` | **$0.002** per page | Only with `ocr: true`, per scanned page that OCR actually read, and only when the PDF itself is processed. |

**Free (never charged):** `password_protected`, `needs_ocr`, `empty`, `not_pdf`, `too_large`, `download_failed`, `timeout`, `parse_error`, `invalid_url`, `blocked_url`, `skipped_budget`.

Examples: 1,000 digital PDFs = **$3.00**. 100 PDFs where 10 are 5-page scans with OCR on = 100 × $0.003 + 50 × $0.002 = **$0.40**.

**Spending limit:** set *Maximum cost per run* (`maxTotalChargeUsd`). The Actor reserves budget before each PDF and OCR page, never exceeds the limit, and marks the remaining URLs `skipped_budget` (free). If the limit is below one PDF ($0.003) the run stops immediately with `BUDGET_TOO_LOW`.

### Statuses

| Status | Charged | Meaning |
| --- | --- | --- |
| `success` | yes | All requested pages read (`truncated: true` if the PDF is longer than `maxPages`). |
| `partial` | yes | Text returned, but some pages are scanned (see `pagesNeedingOcr`), failed (`pageErrors`) or the per-PDF time limit was reached. |
| `needs_ocr` | no | No text layer — the PDF is a scan. Re-run with `ocr: true`. |
| `password_protected` | no | Encrypted with a user password; skipped. (Owner-password "restricted" PDFs that open normally are extracted.) |
| `empty` | no | No text and no images. |
| `not_pdf` | no | The URL returned HTML or another file type. Use the direct PDF link. |
| `too_large` | no | Bigger than `maxFileSizeMb` (default 50 MB, max 200 MB). |
| `download_failed` / `timeout` | no | HTTP error, DNS failure, or time limit reached before any text. Transient errors (429/5xx/network) are retried twice. |
| `parse_error` | no | Corrupted or unsupported PDF. |
| `invalid_url` / `blocked_url` | no | Not an http(s) URL, or a private/internal address. |
| `skipped_budget` | no | Your maximum cost per run was reached. |

### Use it from code, agents and RAG pipelines

#### API (cURL)

```bash
curl -X POST "https://api.apify.com/v2/acts/impressionable_lupine~pdf-text-extractor/run-sync-get-dataset-items?maxTotalChargeUsd=0.10" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pdfUrls":["https://arxiv.org/pdf/1706.03762"],"outputFormat":"markdown","chunkSize":800}'
```

#### JavaScript / TypeScript

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('impressionable_lupine/pdf-text-extractor').call(
    { pdfUrls: ['https://arxiv.org/pdf/1706.03762'], outputFormat: 'markdown', chunkSize: 800, chunkOverlap: 100 },
    { maxTotalChargeUsd: 0.10 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const pdf of items) console.log(pdf.status, pdf.title, pdf.chunkCount);
```

#### Python — load chunks into a vector store

```python
from apify_client import ApifyClient

client = ApifyClient("APIFY_TOKEN")
run = client.actor("impressionable_lupine/pdf-text-extractor").call(
    run_input={"pdfUrls": ["https://arxiv.org/pdf/1706.03762"], "chunkSize": 500, "chunkOverlap": 50},
    max_total_charge_usd=0.10,
)
docs = []
for pdf in client.dataset(run["defaultDatasetId"]).iterate_items():
    if pdf["status"] not in ("success", "partial"):
        continue
    for c in pdf["chunks"]:
        docs.append({
            "id": c["id"],
            "text": c["text"],
            "metadata": {"source": pdf["url"], "title": pdf["title"], "pages": f'{c["pageStart"]}-{c["pageEnd"]}', "section": c["section"]},
        })
## docs -> embed and upsert into Pinecone, Qdrant, pgvector, Chroma, Weaviate ...
```

The **RAG chunks** dataset view (`?view=chunks`) returns one row per chunk, ready for embedding pipelines and no-code tools (Make, n8n, Zapier).

#### MCP — let AI agents read PDFs

Add the Actor to the [Apify MCP server](https://docs.apify.com/integrations/mcp) so Claude, Cursor, ChatGPT or any MCP client can call it as a tool:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=impressionable_lupine/pdf-text-extractor",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

Then ask your agent: *"Read https://arxiv.org/pdf/1706.03762 and summarize section 3 with page references."* The agent receives Markdown plus chunks with page numbers, so it can cite pages. Tip for agents: set `chunkSize: 0` when you only need the full Markdown, and pass `maxTotalChargeUsd` on every call.

### Input options

| Field | Default | Description |
| --- | --- | --- |
| `pdfUrls` | — | List of PDF URLs. |
| `datasetId` / `datasetUrlField` | — / `url` | Read URLs from an Apify dataset (dot paths, arrays). |
| `outputFormat` | `markdown` | `markdown` or `text`. |
| `extractTables` | `true` | Detect tables (rows + Markdown). |
| `chunkSize` / `chunkOverlap` | `800` / `100` | RAG chunk size and overlap in estimated tokens. `chunkSize: 0` disables chunks. |
| `removeHeadersFooters` | `true` | Drop running headers, footers and page numbers. |
| `includePages` | `false` | Add per-page text in `pages[]`. |
| `ocr` / `maxOcrPagesPerPdf` | `false` / `20` | OCR scanned pages (English), with a per-PDF page cap. |
| `maxPages` | `200` | Pages per PDF (`0` = all, hard cap 2,000). |
| `maxFileSizeMb` | `50` | Max download size (1–200 MB). |
| `timeoutSecs` | `180` | Per-PDF time budget (download + extraction). |
| `maxConcurrency` | `3` | PDFs in parallel (fits in the default 1 GB). |

### Limitations

- **Tables are best effort.** Detection uses column alignment, which works well on digitally generated reports, papers and invoices. Merged cells, tables drawn as images, rotated tables and very dense borderless layouts can be split or merged; the text is still in `markdown`/`text`.
- **OCR is English-only** in this version and slower (about 2–5 s per page). Handwriting and low-quality scans give lower `confidence`. Without `ocr: true`, scanned pages are listed in `pagesNeedingOcr`.
- **Layout:** reading order follows the PDF's content stream, which is correct for most papers and reports (including two-column). Unusual producers (some design tools) may interleave text boxes.
- **Not extracted:** images/figures, charts, form-field values typed into fillable forms, annotations, attachments and math as LaTeX.
- **Password-protected PDFs are skipped** — there is no password input by design.
- Only public **http(s)** URLs; private network addresses are blocked. Token counts are estimates (~4 characters per token).

### FAQ

**How do I convert a PDF to Markdown?** Add the URL to `pdfUrls`, keep `outputFormat: markdown`, run. The `markdown` field contains headings, lists and tables.

**How do I extract text from a PDF for RAG?** Use the `chunks` array (or the *RAG chunks* view). Each chunk has text, page range, token estimate and section heading — embed it directly.

**Can it read scanned PDFs?** Yes, with `ocr: true` (+$0.002 per OCR page). Without it, scans return `needs_ocr` and are free.

**What happens with password-protected PDFs?** They return `password_protected` and are not charged.

**Is there a page limit?** `maxPages` (default 200, up to 2,000). The price per PDF does not depend on page count.

**Can I process thousands of PDFs?** Yes — up to 10,000 URLs per run, or feed a dataset from a crawler. For very large batches, raise memory and `maxConcurrency` together.

**Do failed PDFs cost anything?** No. Only `success` and `partial` items are charged; every item shows `charged` and `chargedEvents`.

**Which libraries does it use?** Mozilla PDF.js and Tesseract (both Apache-2.0). No AGPL software. Full list: [LICENSES.md](LICENSES.md).

**Is my data stored?** PDFs are processed in memory during the run. Results stay in your run's dataset under your Apify data-retention settings.

### Support

Found a PDF that extracts poorly? Open an issue on the Actor page with the URL and what you expected — layout fixes ship regularly.

# Actor input Schema

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

Direct links to PDF files (http/https). arXiv /abs/ links, Google Drive share links, Dropbox ?dl=0 links and GitHub blob links are converted to direct downloads automatically. Duplicates are removed. Up to 10,000 per run.

## `datasetId` (type: `string`):

ID or name of an Apify dataset whose items contain PDF URLs, e.g. the output of a crawler or another Actor. Combined with the list above.

## `datasetUrlField` (type: `string`):

Field that holds the PDF URL. Dot paths (e.g. "document.pdfUrl") and arrays of URLs are supported.

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

Markdown keeps headings, lists and tables (best for LLMs and RAG). Text is plain reflowed text (tables are tab-separated).

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

Detect tables from column alignment and return them as rows (and Markdown tables). Best effort: works on most digitally-created PDFs; merged cells and borderless complex layouts may split.

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

Target maximum tokens per chunk (estimate: ~4 characters per token). Chunks follow paragraph, heading and sentence boundaries. Set 0 to disable chunks.

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

Tokens repeated from the end of the previous chunk at the start of the next one. Capped at half the chunk size.

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

Drops lines that repeat in the top/bottom margin of most pages, plus bare page numbers. Recommended for RAG.

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

Adds a pages\[] array with the text of each page and its kind (text, ocr, needs_ocr, blank). Increases item size.

## `ocr` (type: `boolean`):

Run OCR (Tesseract, English) on pages that have no text layer. Adds $0.002 per OCR page read. Off: scanned pages are listed in pagesNeedingOcr and a fully scanned PDF gets status needs_ocr (free).

## `maxOcrPagesPerPdf` (type: `integer`):

Upper bound on OCR pages per PDF to keep cost and time predictable.

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

Only the first N pages are processed (item.truncated = true when the PDF is longer). 0 = all pages, up to the hard cap of 2,000.

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

Larger files are skipped with status too_large (not charged).

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

Download + extraction budget per PDF. If reached after some pages, the item is returned with status partial; with no text, status timeout (not charged).

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

PDFs processed at the same time. 3 fits comfortably in 1 GB; raise memory before raising this.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "datasetUrlField": "url",
  "outputFormat": "markdown",
  "extractTables": true,
  "chunkSize": 800,
  "chunkOverlap": 100,
  "removeHeadersFooters": true,
  "includePages": false,
  "ocr": false,
  "maxOcrPagesPerPdf": 20,
  "maxPages": 200,
  "maxFileSizeMb": 50,
  "timeoutSecs": 180,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

Status, metadata, counts and charge per PDF.

## `content` (type: `string`):

Complete items with text/markdown, tables and chunks.

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

One row per chunk, ready for embedding.

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

Counts by status and billed events.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("impressionable_lupine/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"] }

# Run the Actor and wait for it to finish
run = client.actor("impressionable_lupine/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"
  ]
}' |
apify call impressionable_lupine/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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