# PDF Text Extractor - Markdown, RAG Chunks, OCR & Metadata (`artificially/pdf-text-extractor`) Actor

Extract clean text or Markdown from PDF (and DOCX) URLs for AI agents and RAG: reading order across columns, de-hyphenation, header/footer removal, page ranges, per-page output or token-sized chunks with overlap, metadata, links, scanned-page detection and optional OCR. One item per document.

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

## Pricing

from $2.17 / 1,000 documents

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 Text Extractor – Markdown, RAG Chunks, OCR & Metadata

Turn PDF (and Word .docx) URLs into **clean text or Markdown for AI agents and RAG pipelines**. Paste document links and get one dataset item per document with the text in reading order, headings and lists, best-effort tables, token-sized chunks with overlap, per-page text, metadata and links. Scanned pages are detected, and you can turn on OCR to read them.

**Built by [Artificially](https://apify.com/artificially)** | Follow us on [X @artificially\_io](https://x.com/artificially_io)

#### Use with AI agents (MCP)

This actor works as a tool for Claude, ChatGPT, Cursor and other AI agents through [Apify's MCP server](https://mcp.apify.com). Ask something like "read this RFP PDF and list the submission deadlines" and the agent calls it for you. Set `compactOutput: true`, `outputFormat: "markdown"` and a small `maxPages` (or a `pageRange` such as `"1-10"`) so results fit easily in the agent's context. For long documents use `splitBy: "chunk"` and let the agent read only the chunks it needs.

**Quick setup** (sign in with your Apify account when asked):

- **Claude** (claude.ai or Claude Desktop): Settings → Connectors → Add custom connector, and paste `https://mcp.apify.com?tools=artificially/pdf-text-extractor`
- **Claude Code or Cursor** via the Apify CLI (latest version, `apify upgrade`): `apify mcp install claude-code --tools artificially/pdf-text-extractor` (use `cursor` instead of `claude-code` for Cursor)
- **Any MCP client** (Cursor, VS Code, Windsurf):

```json
{
  "mcpServers": {
    "apify": { "url": "https://mcp.apify.com?tools=artificially/pdf-text-extractor" }
  }
}
```

**Try asking:**

- "Extract the text of this PDF and summarize the key requirements: https://arxiv.org/pdf/1706.03762"
- "Split these RFP PDFs into chunks for my knowledge base."

### Why this actor

- **Reading order that holds up** – text is rebuilt from positions on the page: lines grouped by baseline, 2, 3 and 4-column layouts detected (including narrow justified columns like IRS publications), superscripts attached, words split across lines re-joined ("infor-mation" becomes "information", "English-to-German" stays hyphenated).
- **Clean for RAG** – repeated running headers, footers and page numbers are removed, TOC leader dots are collapsed, letter-spaced headings ("G r u n d g e s e t z") are fixed, paragraphs broken by page breaks are merged.
- **Markdown with structure** – `#` headings from font size and section numbering (`3.1`, `3.2.1`), bullet and numbered lists, and pipe tables where the table grid is clear.
- **Chunks ready to embed** – `splitBy: "chunk"` returns chunks of about N tokens with overlap, split at paragraph and sentence boundaries, each with `pageStart`, `pageEnd` and the `section` heading it belongs to.
- **Scanned PDFs handled honestly** – image-only pages are detected and listed. With OCR off, fully scanned documents are free and reported as `needs_ocr`. With OCR on, only pages without a text layer are OCRed.
- **Any download link** – redirects are followed, so signed links work (SAM.gov attachment links 303-redirect to S3). arXiv abstract links and IDs, Google Drive, Dropbox and GitHub file links are converted to direct downloads.
- **Word files too** – `.docx` documents are detected automatically and converted with headings, lists, tables and links.
- **You only pay for results** – one charge per document with extracted text, whatever its page count. 404s, HTML pages, oversized files, password-protected files and scanned files without OCR cost nothing and are listed in `FAILED_DOCUMENTS`.

### Use cases

- **RAG and knowledge bases** – ingest reports, manuals, papers and policies into Pinecone, Weaviate, Qdrant, pgvector or Chroma with `splitBy: "chunk"`.
- **Government contracting** – extract statements of work, RFPs and amendments attached to SAM.gov notices and feed them to an LLM for bid/no-bid analysis.
- **Research** – turn arXiv papers into Markdown with section headings for summarization and literature reviews.
- **Legal and compliance** – convert laws, regulations and contracts (any language) into clean text for search and review.
- **Document monitoring** – schedule a run over a list of PDF links and diff the extracted text over time.

### Input

| Field | Type | Description | Default |
|-------|------|-------------|---------|
| `urls` | array | PDF or DOCX URLs. Redirects followed; arXiv abs links/IDs, Google Drive, Dropbox and GitHub links converted | Required |
| `outputFormat` | string | `text` or `markdown` | `text` |
| `splitBy` | string | `document` (whole text), `page` (`pages` array) or `chunk` (`chunks` array). Always one dataset item per document | `document` |
| `chunkSize` | integer | Target chunk size in tokens (1 token ≈ 4 characters) | `1000` |
| `chunkOverlap` | integer | Tokens repeated between consecutive chunks (max half of `chunkSize`) | `100` |
| `includeMetadata` | boolean | Title, author, subject, keywords, creator, producer, dates, PDF version, encryption, tagged, form | `true` |
| `extractLinks` | boolean | Clickable links and URLs in the text, with the page they are on | `false` |
| `compactOutput` | boolean | Slim items with only the key fields and the content. Best for AI agents | `false` |
| `pageRange` | string | Pages to extract, e.g. `"1-5"`, `"1-3,8,10-12"`, `"20-"` | all |
| `maxPages` | integer | Page limit per document (1–1000), applied after `pageRange` | `200` |
| `removeHeadersFooters` | boolean | Drop lines repeated at the top/bottom of at least 40% of pages, and page numbers | `true` |
| `ocr` | boolean | OCR pages without a text layer (Tesseract). Needs 1024 MB memory or more | `false` |
| `ocrLanguage` | string | Tesseract language code(s): `eng`, `deu`, `fra`, `spa`, `eng+deu`… | `eng` |
| `ocrMaxPages` | integer | Maximum pages OCRed per document | `20` |
| `pdfPassword` | string | Password for PDFs that need one to open | – |
| `maxFileSizeMb` | integer | Larger files are skipped (free) | `50` |

#### Example input

```json
{
    "urls": [
        "https://arxiv.org/abs/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/p17.pdf",
        "https://sam.gov/api/prod/opps/v3/opportunities/resources/files/0cf491f2ff9e4b5fb8d43afbfa917762/download"
    ],
    "outputFormat": "markdown",
    "splitBy": "chunk",
    "chunkSize": 800,
    "chunkOverlap": 80
}
```

### Output

One item per document. The content is in `text` or `markdown` (`splitBy: "document"`), `pages` (`splitBy: "page"`) or `chunks` (`splitBy: "chunk"`); the other content fields are `null`.

```json
{
    "url": "https://arxiv.org/abs/1706.03762",
    "finalUrl": "https://arxiv.org/pdf/1706.03762",
    "fileName": "1706.03762v7.pdf",
    "fileType": "pdf",
    "fileSizeBytes": 2215244,
    "pageCount": 15,
    "pagesExtracted": 15,
    "charCount": 39874,
    "wordCount": 6042,
    "tokenEstimate": 10123,
    "language": "en",
    "needsOcr": false,
    "pagesWithoutText": [],
    "ocrPages": null,
    "metadata": {
        "title": null,
        "author": null,
        "subject": null,
        "keywords": null,
        "creator": "LaTeX with hyperref",
        "producer": "pdfTeX-1.40.25",
        "createdAt": "2024-04-10T21:11:43.000Z",
        "modifiedAt": "2024-04-10T21:11:43.000Z",
        "pdfVersion": "1.5",
        "isEncrypted": false,
        "isTagged": null,
        "hasForm": false
    },
    "outputFormat": "markdown",
    "splitBy": "chunk",
    "text": null,
    "markdown": null,
    "pages": null,
    "chunks": [
        {
            "index": 3,
            "text": "## 1 Introduction\n\nRecurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks in particular, have been firmly established as state of the art approaches in sequence modeling and transduction problems...",
            "charCount": 3150,
            "tokenEstimate": 788,
            "pageStart": 2,
            "pageEnd": 2,
            "section": "1 Introduction"
        }
    ],
    "links": null,
    "headerFooterLinesRemoved": 14,
    "truncated": false,
    "scrapedAt": "2026-09-23T11:20:05.118Z"
}
```

| Field | Description |
|-------|-------------|
| `url`, `finalUrl` | The URL you gave and the URL the file was downloaded from after redirects (signed query strings are removed) |
| `fileName`, `fileType`, `fileSizeBytes` | From the download headers; `fileType` is `pdf` or `docx` |
| `pageCount`, `pagesExtracted` | Pages in the file and pages read (after `pageRange`/`maxPages`); `null` for DOCX |
| `charCount`, `wordCount`, `tokenEstimate` | Size of the extracted content (tokens ≈ characters / 4) |
| `language` | Detected language (ISO 639-1, e.g. `en`, `de`, `fr`, `zh`) or `null` |
| `needsOcr`, `pagesWithoutText` | Pages with no text layer; `needsOcr` is `true` when they contain images (scans) |
| `ocrPages` | Pages read with OCR (when `ocr` is on) |
| `metadata` | Document properties (PDF only) |
| `text` / `markdown` | Full content for `splitBy: "document"` |
| `pages` | `[{ page, text, charCount, ocr }]` for `splitBy: "page"` |
| `chunks` | `[{ index, text, charCount, tokenEstimate, pageStart, pageEnd, section }]` for `splitBy: "chunk"` |
| `links` | `[{ url, page }]` when `extractLinks` is on |
| `truncated` | `true` if the document was cut to stay within the time limit or the 9 MB dataset item limit |

The key-value store also contains:

- `SUMMARY` – documents saved and failed, failures by reason, pages, OCR pages, characters, settings and whether the cost limit was reached.
- `FAILED_DOCUMENTS` – each URL that was not extracted, with a reason: `invalid_url`, `not_found`, `access_denied`, `http_error`, `timeout`, `too_large`, `not_a_pdf`, `empty_file`, `password_protected`, `corrupted`, `needs_ocr`, `no_text`, `page_range_empty` or `processing_error`. These are never charged.

### Scanned PDFs and OCR

Every page is checked for a text layer. When a page has almost no text and contains an image, it is a scan:

- **OCR off (default):** documents that are mostly scanned (half of the pages or more) are not saved and not charged; they appear in `FAILED_DOCUMENTS` as `needs_ocr`. Documents with only a few scanned pages are saved with `needsOcr: true` and the scanned page numbers in `pagesWithoutText`.
- **OCR on:** pages without a text layer are rendered and read with Tesseract (`ocrLanguage`), up to `ocrMaxPages` per document. Each OCRed page is charged as an OCR page. OCR needs at least 1024 MB of memory and takes a few seconds per page, so set the run memory to 1024 MB or more. OCR quality on handwriting and poor scans is limited.

### Tables

Tables are detected from the layout of the text (aligned columns across several rows) and output as pipe tables in Markdown or `cell | cell` rows in text. This works well for simple grids and financial or statistical tables, and rows always stay together, but merged cells, multi-line cells and tables without clear column gaps may be split or joined differently from the original. PDFs have no table structure inside, so treat tables as best-effort.

### Pricing

This actor uses pay-per-event pricing: a small fee per run, a fee per **document** with extracted text (the same price for a 2-page memo and a 200-page report), and a fee per OCR page only when you turn OCR on. **You only pay for results** – failed downloads, non-PDF links, password-protected, empty and scanned-without-OCR documents are free. Set a maximum cost per run in Apify Console and the actor stops cleanly when it is reached. See the **Pricing** tab for current prices.

### FAQ

**Which PDFs work best?**
Digitally created PDFs (exported from Word, LaTeX, InDesign, report generators) give near-perfect text. Scanned PDFs need OCR.

**How do I get SAM.gov attachments?**
Use the attachment download links from a notice (`https://sam.gov/api/prod/opps/v3/opportunities/resources/files/{resourceId}/download`). The actor follows the redirect to the signed file. The [SAM.gov Contract Opportunities Scraper](https://apify.com/artificially/samgov-opportunities-scraper) returns these links for each notice.

**Can it read password-protected PDFs?**
PDFs that only restrict printing or copying open without a password. PDFs that need a password to open are extracted when you set `pdfPassword`; otherwise they are listed as `password_protected` for free.

**What about very long documents?**
`maxPages` (default 200, up to 1000) and `pageRange` limit the work per document. Dataset items are capped at about 9 MB; bigger outputs are cut and marked `truncated: true`. Use `splitBy: "page"` or `"chunk"` with a `pageRange` to process a huge document in parts.

**Does it support non-English documents?**
Yes. Text extraction works for any language, including CJK scripts. Language detection covers the main European languages and common scripts. For OCR set `ocrLanguage` (for example `deu` or `fra`).

**How much memory should I use?**
512 MB is enough for text extraction, including batches of 100+ page reports. Use 1024 MB or more when OCR is on.

### Support

Found a PDF that comes out wrong? Open an issue on the actor's **Issues** tab with the URL – we usually respond within a day.

### Related actors

- [SAM.gov Contract Opportunities Scraper](https://apify.com/artificially/samgov-opportunities-scraper): federal contract notices; their RFP, SOW and amendment attachments are PDF links you can pass straight to this actor.
- [arXiv Papers Scraper](https://apify.com/artificially/arxiv-scraper): search arXiv papers; every item has a `pdfUrl` to extract with this actor.
- [EU Tenders Scraper](https://apify.com/artificially/eu-tenders-scraper): public procurement notices from the EU, with tender documents to read with this actor.

# Actor input Schema

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

Required. One public URL per entry pointing to a PDF (or a Word .docx) file. Redirects are followed, so signed download links work (e.g. SAM.gov attachment URLs like https://sam.gov/api/prod/opps/v3/opportunities/resources/files/{resourceId}/download that 303-redirect to S3). arXiv abstract pages (https://arxiv.org/abs/1706.03762) and bare arXiv IDs (1706.03762) are mapped to the PDF automatically; Google Drive file links, Dropbox share links and GitHub blob links are converted to direct downloads. Duplicate URLs are processed once. Example: \["https://arxiv.org/pdf/1706.03762", "https://www.irs.gov/pub/irs-pdf/fw9.pdf"].

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

"text" returns clean plain text (paragraphs separated by blank lines, lists as "- item", tables as "cell | cell" rows). "markdown" returns Markdown with # headings (detected from font size and section numbering like "3.1"), lists and best-effort pipe tables. Use markdown for LLMs and RAG when document structure matters.

## `splitBy` (type: `string`):

How the extracted content is returned. There is always ONE dataset item per document. "document": the whole text in the text/markdown field. "page": a pages array \[{page, text}] (text/markdown field is null). "chunk": a chunks array \[{index, text, pageStart, pageEnd, section, tokenEstimate}] ready for embedding in a vector database (text/markdown field is null).

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

Only used when splitBy is "chunk". Target chunk size in tokens (1 token is estimated as 4 characters, so 1000 tokens is about 4000 characters). Chunks break at paragraph and sentence boundaries and start a new chunk at headings. Example: 500 for small-context embedding models, 1000 as a general default.

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

Only used when splitBy is "chunk". How many tokens of the end of the previous chunk are repeated at the start of the next one, so context is not lost at boundaries. Max half of chunkSize. Example: 100.

## `includeMetadata` (type: `boolean`):

Add a metadata object: title, author, subject, keywords, creator, producer, createdAt, modifiedAt (ISO 8601), pdfVersion, isEncrypted, isTagged, hasForm. Not available for DOCX (null).

## `extractLinks` (type: `boolean`):

Add a links array \[{url, page}] with every clickable link (PDF link annotations, DOCX hyperlinks) and every http(s) URL written in the text, de-duplicated.

## `compactOutput` (type: `boolean`):

Return slim items with only url, fileName, fileType, title, pageCount, language, charCount, needsOcr, truncated and the content (text/markdown, pages or chunks). Best for AI agents and MCP clients with limited context.

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

Extract only these pages (1-based). Comma-separated pages and ranges; an open range runs to the last page. Examples: "1-5", "1-3,8,10-12", "20-". Leave empty for all pages (up to maxPages). Applies to every PDF in the run.

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

Stop after this many pages per document (applied after pageRange). Protects run time on very long files. Example: 20 to read only the beginning of long reports.

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

Drop lines at the top or bottom of pages that repeat on at least 40% of pages (running titles, document IDs, "Page 3 of 20", bare page numbers). Needs 3 or more pages. Recommended for RAG so chunks are not polluted.

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

Run OCR (Tesseract) on pages that have no text layer (scanned or image-only). Pages that already have text are never OCRed. Charged per OCR page (see Pricing). Needs at least 1024 MB memory; slower (several seconds per page). When off, scanned documents are reported in FAILED\_DOCUMENTS as needs\_ocr and are not charged.

## `ocrLanguage` (type: `string`):

Tesseract language code(s) of the scanned documents, joined with + for several. Examples: "eng", "deu", "fra", "spa", "eng+deu".

## `ocrMaxPages` (type: `integer`):

Upper limit of pages sent to OCR in one document, to keep time and cost predictable. Example: 5 to OCR only the first scanned pages.

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

Password used to open password-protected PDFs (applied to every PDF in the run that asks for one). PDFs that are only permission-protected (no password needed to open) are extracted without it.

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

Files larger than this are skipped (listed as too\_large in FAILED\_DOCUMENTS, not charged). Downloads are streamed and stopped as soon as the limit is passed.

## Actor input object example

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputFormat": "text",
  "splitBy": "document",
  "chunkSize": 1000,
  "chunkOverlap": 100,
  "includeMetadata": true,
  "extractLinks": false,
  "compactOutput": false,
  "maxPages": 200,
  "removeHeadersFooters": true,
  "ocr": false,
  "ocrLanguage": "eng",
  "ocrMaxPages": 20,
  "maxFileSizeMb": 50
}
```

# Actor output Schema

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

One item per extracted document (overview table view)

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

All fields including text, markdown, pages, chunks and links

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

Counts of saved and failed documents, pages, OCR pages and failure reasons

## `failedDocuments` (type: `string`):

URLs that could not be extracted, with a reason such as not\_found, not\_a\_pdf, too\_large, password\_protected or needs\_ocr (never charged)

# 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": [
        "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("artificially/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 = { "urls": [
        "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("artificially/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 '{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call artificially/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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