# PDF Text Extractor with OCR | 💵$0.005 per PDF (`webdata_labs/pdf-text-extractor`) Actor

\[💵 $0.005 per PDF + $0.0004 per page · no start fee] Any PDF URL into clean, layout-aware text that reads multi-column pages in the right order. Whole document and page by page, plus metadata, bookmarks and links. Optional OCR reads scanned pages, billed only for the pages it reads.

- **URL**: https://apify.com/webdata\_labs/pdf-text-extractor.md
- **Developed by:** [WebData Labs](https://apify.com/webdata_labs) (community)
- **Categories:** Developer tools, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.20 / 1,000 pdfs

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF Text Extractor

**Turn any list of PDF URLs into clean, layout-aware text plus full metadata, bookmarks and links - and get told which files are scanned images before you wonder why the text is empty.**

Most PDF extractors hand you one welded string per file. Columns run into each other, every paragraph break is gone, and a scanned document comes back as an empty `text` field with nothing to explain why. This Actor rebuilds the page layout instead of concatenating glyph runs: lines stay lines, paragraphs stay paragraphs, and each page arrives separately with its own character and word counts. Files that carry images but no text are labelled `needsOcr: true` rather than returned silently blank, encrypted files are opened when you supply the password and reported honestly when you do not, and anything that failed to download comes back as an error row you are never charged for. Scanned pages can be read too: turn `ocr` on and only the pages that actually needed it are recognised and billed. No API key, no browser, no proxy.

### ✅ What you get / ❌ what this isn't

| ✅ What you get | ❌ What this isn't |
|---|---|
| **Layout-aware `text`** with real line and paragraph breaks | Not `items.join(" ")`, which welds every line into one blob |
| **Multi-column pages in reading order**, one column finished before the next | Not two columns spliced into each other line by line |
| **`pages[]`** with the text, `charCount`, `wordCount` and size of each page | Not a single document string you have to re-split yourself |
| **`needsOcr`** set when unreadable pages actually carry images | Not an empty `text` field with no explanation |
| **Optional OCR** that reads scanned pages, billed only for pages it actually read | Not a scan you have to route to a second tool and pay for twice |
| **`passwords`** tried against encrypted files, with `wasDecrypted` recorded | Not a hard failure on every protected PDF |
| **`outline[]`** - the bookmark tree with the page each entry points to | Not metadata only, with the document structure thrown away |
| **`links[]`** - every clickable link annotation with its page | Not URLs you have to regex back out of the text |
| **`chunks[]`** - page-tagged chunks sized for embeddings, on request | Not a raw dump you must split before it reaches a vector store |
| **Priced per PDF and per page**, so short files stay cheap | Not one flat fee that makes a 1-page invoice subsidise a 300-page manual |
| **No start fee**, and failed downloads are never charged | Not a per-run fee on top of the per-file price |

### 🔎 Why use this Actor

- **The layout is rebuilt, not guessed.** PDF stores positioned glyph runs, not lines. The page is first cut into blocks: a vertical gutter separates columns, a full-width horizontal band separates a banner headline from the body beneath it. Only inside a block are runs bucketed into lines, ordered left to right, and their vertical gaps turned back into paragraph breaks. That is the difference between text an LLM can read and text it has to untangle.
- **Two-column papers come out readable.** Grouping lines by baseline alone, which is what almost every extractor does, splices the left column's line into the right column's sentence. Here each column is finished before the next one starts, and words broken across a line break are rejoined.
- **Scanned files are named as scanned.** A page under 40 characters is probed for image operators, and `needsOcr` is only set when those pages really do carry images. A blank cover sheet is not flagged, and a 300-page scan is not delivered as a successful empty run.
- **Password-protected PDFs are supported.** Put candidate passwords into `passwords` and each file is opened with the first one that works. `isEncrypted` and `wasDecrypted` are recorded either way, so you can tell "protected and opened" from "protected and skipped".
- **Every failure is a row, and a free one.** A 404, a timeout, an HTML login page served instead of a file, a corrupt document: each returns a row with `status: "error"` and a specific `errorType`, the run keeps going, and no charge event fires. You pay for PDFs that were actually read.
- **Ready for RAG without a second step.** Set `chunkChars` and the text comes back pre-split on paragraph boundaries, never across a page, with the page number on every chunk so your citations survive.
- **Competitor input JSON just runs.** `urls`, `startUrls`, `pdfUrls`, `fileUrls`, `links`, `maxResults`, `maxItems` and `maxUrls` are all accepted, so a config copied from another PDF Actor's docs does not fail validation on the first run.
- **Nothing to tune.** No proxy, concurrency, retry or timeout fields. Pacing, retries with backoff and download limits are handled internally and included in the price. It runs on plain HTTP with no browser, which is why it is fast and cheap.

### 👥 Who it's for

AI and RAG engineers, legal and compliance teams, financial and procurement analysts, researchers and developers who have a list of PDF links and need the text behind them in a structured, machine-readable shape.

- Load contracts, filings, tenders or manuals into a vector store with page-level citations intact.
- Feed report text straight into an LLM for summarisation, classification or extraction.
- Screen a document set, separate the digital-native files from the scans, and read the scans in the same run when you want them.
- Build a searchable index of every PDF a website publishes, with title, author and creation date attached.
- Pull the bookmark tree out of long documents to map their structure before reading any of them.
- Extract every outbound link from a document set for reference or citation analysis.

### Example tasks

Ready-made configurations, each set up for one job:

- [Extract text from a list of PDF URLs](https://apify.com/webdata_labs/pdf-text-extractor/examples/extract-text-from-pdf-urls)
- [Convert PDFs to text for RAG and embeddings](https://apify.com/webdata_labs/pdf-text-extractor/examples/pdf-to-text-for-rag-chunks)
- [Extract PDF metadata: title, author and dates](https://apify.com/webdata_labs/pdf-text-extractor/examples/extract-pdf-metadata)
- [Find scanned PDFs that need OCR](https://apify.com/webdata_labs/pdf-text-extractor/examples/detect-scanned-pdfs-needing-ocr)
- [Extract text from password-protected PDFs](https://apify.com/webdata_labs/pdf-text-extractor/examples/password-protected-pdf-extractor)
- [Extract text page by page from a PDF](https://apify.com/webdata_labs/pdf-text-extractor/examples/pdf-page-by-page-text)
- [Extract bookmarks and the table of contents from a PDF](https://apify.com/webdata_labs/pdf-text-extractor/examples/extract-pdf-bookmarks-outline)
- [Extract all hyperlinks from PDF documents](https://apify.com/webdata_labs/pdf-text-extractor/examples/extract-links-from-pdf)
- [Extract text from research paper PDFs](https://apify.com/webdata_labs/pdf-text-extractor/examples/extract-text-from-research-papers)
- [Bulk PDF text extraction to CSV or Excel](https://apify.com/webdata_labs/pdf-text-extractor/examples/bulk-pdf-text-extraction-to-csv)

### ⚙️ How to extract text from a PDF

1. Open the Actor and paste your PDF links into `urls`. Any public HTTP or HTTPS link works, including files on S3, Google Cloud Storage, an Apify key-value store or your own server. Redirects are followed.
2. Leave `maxPdfs` at 100 for a first run. It is a budget guard, not a hard ceiling: raise it for a big batch, or set 0 to process the whole list.
3. Leave `includePageText` on if you want each page separately. Turn it off for smaller rows when the whole-document `text` is all you need.
4. If any of your files are protected, add the candidate passwords to `passwords`.
5. Click **Start**. Results appear in the **Documents** view on the Output tab, with the full text in the **Extracted text** view, and export as JSON, CSV, Excel, XML or RSS.

#### Load a document set into a vector store

Set `chunkChars` to your embedding model's window, for example 1200. Each row then carries a `chunks` array where every chunk is a paragraph-aligned slice of a single page, tagged with `page` and `index`. Feed `chunks` straight into your embedding step: the page number travels with the text, so answers can cite a page rather than a file.

#### Process a folder of PDFs on a schedule

Create a Saved Task with your URL list, then add a schedule on the Actor's **Schedules** tab. Because `sha256` is returned for every file, you can detect a document that changed between runs without diffing its text.

### 📥 Input

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "maxPdfs": 100,
  "includePageText": true,
  "includeOutline": true,
  "includeLinks": true,
  "chunkChars": 1200,
  "ocr": false,
  "ocrLanguage": "eng",
  "maxOcrPages": 50,
  "passwords": []
}
```

- `urls` - the PDF links to read. Accepts full URLs and bare domains with a path. Aliases `startUrls` (plain strings or `{ "url": ... }` objects), `pdfUrls`, `fileUrls` and `links` are merged into the same list and deduplicated.
- `maxPdfs` - budget cap on how many PDFs this run processes. Default `100`. Set `0` for no cap. Aliases: `maxResults`, `maxItems`, `maxUrls`.
- `includePageText` - add the `pages` array with per-page text and counts. Default `true`.
- `includeOutline` - extract the bookmark tree into `outline`. Default `true`.
- `includeLinks` - extract clickable link annotations into `links`. Default `true`.
- `chunkChars` - optional. Split the text into page-tagged chunks of about this many characters for embeddings. Default `0`, which skips chunking. Values below 200 are raised to 200.
- `ocr` - read scanned pages by recognising the page image. Default `false`, because OCR costs extra and most PDFs do not need it. Only pages with no text layer that actually contain a scan are processed and billed.
- `ocrLanguage` - language of the scanned text: `eng`, `deu`, `fra`, `spa`, `ita`, `por`, `nld`, `pol`, `ukr` or `rus`. Language names (`English`, `German`) and two-letter codes (`en`, `de`) are accepted too. Default `eng`. Only used when `ocr` is on.
- `maxOcrPages` - cap on scanned pages read per file, so one long scan cannot surprise you. Default `50`. Set `0` for no cap. When the cap stops the work the row carries `ocrTruncated: true`.
- `passwords` - optional list of passwords tried in order against encrypted files. Default `[]`.

### 📤 Output

One row per input URL. A short sample of the most-scanned fields:

| url | fileName | pageCount | title | charCount | needsOcr | isEncrypted | status |
|---|---|---|---|---|---|---|---|
| https://arxiv.org/pdf/1706.03762 | 1706.03762v7.pdf | 15 | null | 39720 | false | false | ok |
| https://www.irs.gov/pub/irs-pdf/fw9.pdf | fw9.pdf | 6 | Form W-9 (Rev. March 2024) | 37704 | false | false | ok |
| https://example.com/scan.pdf | scan.pdf | 6 | imagemagick-images | 0 | true | false | ok |
| https://example.com/missing.pdf | null | null | null | null | null | null | error |

Every row carries the full field set below.

| Field | Type | What it is |
|---|---|---|
| `recordType` | string | Always `pdf`. |
| `url` | string | The URL exactly as you supplied it. |
| `finalUrl` | string | The URL the download landed on after redirects. |
| `status` | string | `ok` or `error`. |
| `httpStatus` | integer | HTTP status of the download. |
| `fileName` | string | From `Content-Disposition` if the server sent one, otherwise from the URL path. |
| `fileSizeBytes` | integer | Size of the downloaded file. |
| `contentType` | string | Content type the server declared. |
| `sha256` | string | Hex digest of the file, for change detection and deduplication. |
| `pageCount` | integer | Pages in the document. |
| `processedPages` | integer | Pages actually read. Equals `pageCount` unless the per-file time budget was hit. |
| `truncatedAtPage` | integer | The page reading stopped at, or `null` when the whole file was read. |
| `pdfVersion` | string | PDF format version, for example `1.7`. |
| `isEncrypted` | boolean | The file uses PDF encryption. |
| `wasDecrypted` | boolean | One of your `passwords` was needed to open it. |
| `isLinearized` | boolean | The file is optimised for fast web viewing. |
| `hasAcroForm` | boolean | The file contains form fields (AcroForm or XFA). |
| `hasSignature` | boolean | The file carries a digital signature. |
| `title`, `author`, `subject`, `keywords` | string | Document information dictionary, `null` when absent. |
| `creator`, `producer` | string | The application that authored the content and the one that wrote the PDF. |
| `language` | string | Document language tag, for example `en-US`. |
| `createdAt`, `modifiedAt` | string | Creation and modification timestamps, normalised to ISO 8601 UTC. |
| `text` | string | The whole document as layout-aware text, pages joined by a blank line. |
| `textTruncated` | boolean | The text hit the 3,000,000 character row limit and was cut. |
| `charCount` | integer | Characters extracted across all pages. |
| `wordCount` | integer | Whitespace-separated tokens in `text`. |
| `avgCharsPerPage` | integer | `charCount` divided by `processedPages`. |
| `emptyTextPages` | integer | Pages that yielded no text at all. |
| `needsOcr` | boolean | The document has unreadable pages that carry images and were **not** read. With `ocr` on it means "still unread", so a fully OCR'd scan comes back `false`. |
| `ocrUsed` | boolean | At least one page was read by OCR. |
| `ocrPages` | integer | Pages OCR actually read, and exactly what you are charged for. |
| `ocrCharCount` | integer | Characters that came from OCR rather than from the file's own text layer. |
| `ocrConfidence` | integer | Average confidence over the OCR'd pages, 0-100. Low values mean a poor scan - see the caveats. |
| `ocrLanguage` | string | The language OCR ran with, or `null` when it was not used. |
| `ocrTruncated` | boolean | `maxOcrPages` stopped the work with scanned pages left over. |
| `pages` | array | `{ page, text, charCount, wordCount, width, height, rotation, ocrApplied, ocrConfidence }` per page, when `includePageText` is on. |
| `outline` | array | `{ level, title, page, url }` per bookmark, nested depth flattened into `level`. |
| `links` | array | `{ page, url }` per clickable link annotation, deduplicated. |
| `linkCount` | integer | Number of entries in `links`. |
| `chunks` | array | `{ index, page, charCount, text }` per chunk, when `chunkChars` is set. |
| `chunkCount` | integer | Number of entries in `chunks`. |
| `error` | string | Human-readable failure message, `null` on success. |
| `errorType` | string | One of `not_found`, `http_error`, `timeout`, `network_error`, `not_a_pdf`, `file_too_large`, `password_required`, `invalid_pdf`, `parse_error`. |
| `durationMs` | integer | Download plus extraction time for this file. |
| `extractedAt` | string | ISO 8601 timestamp of when the row was produced. |

The run also writes a `summary` record to the key-value store with `requested`, `extracted`, `failed`, `pagesRead`, `charactersExtracted`, `needsOcr`, `encrypted` and `ocrPages` counts.

### 💵 How much does it cost?

Charges drop with your Apify plan and there is **no start fee**: a small amount per PDF opened, plus a per-page amount for what is actually inside it. A one-page invoice costs a fraction of a 300-page manual, which a flat per-file price cannot express. OCR is charged separately and only when you turn it on.

| | FREE | BRONZE | SILVER | GOLD | PLATINUM | DIAMOND |
|---|---|---|---|---|---|---|
| **per PDF** | $0.005 | $0.0044 | $0.0038 | $0.0032 | $0.0026 | $0.002 |
| **per page** | $0.0004 | $0.00035 | $0.0003 | $0.00025 | $0.0002 | $0.00016 |
| **per OCR page** | $0.010 | $0.0088 | $0.0076 | $0.0064 | $0.0052 | $0.004 |

The OCR charge applies **only to pages OCR actually read**: a page that already had text, a blank page, and a page the engine could not read cost you nothing extra. With `ocr` off it never applies at all.

What that works out to on the free tier:

| Document | Here | An Actor at $0.01 per PDF + $0.0005 per page |
|---|---|---|
| 1-page invoice | $0.0054 | $0.0105 |
| 26-page report | $0.0154 | $0.0230 |
| 142-page manual | $0.0618 | $0.0810 |

Rows that failed to download or parse carry `status: "error"` and are **not charged** - neither the file nor its pages - so a stale link list costs you nothing beyond the platform compute. Pages that could not be read inside the per-file time budget are not charged either.

Apify platform usage (compute and data transfer) is billed separately by Apify and is small: there is no browser and no proxy, and a 15-page research paper is read in well under a second.

### 🔁 Run it on the Apify platform

Schedule the Actor for a recurring document sweep, call it from the Apify API or any [Apify client](https://docs.apify.com/api/client/js), and export results as JSON, CSV, Excel, XML or RSS. Datasets connect to Make, Zapier, Google Sheets, Airtable, Slack and webhooks, so extracted text can land in a spreadsheet, a warehouse or a vector store without glue code.

#### Use with AI agents via MCP

The Actor is exposed over Apify's MCP server, so an agent can call it directly and read a PDF on its own.

**Claude Code**

```bash
claude mcp add apify --transport http https://mcp.apify.com --header "Authorization: Bearer <YOUR_APIFY_TOKEN>"
```

**Cursor or Claude Desktop** - add to your MCP config:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Then ask the agent to run `webdata_labs/pdf-text-extractor` with the PDF URL. Structured parameters exist precisely so an agent can fill `urls: ["https://..."]` without constructing anything.

#### Use this Actor in n8n

n8n ships an Apify node. Add it, authenticate with your Apify API token, choose **Run Actor**, select `webdata_labs/pdf-text-extractor`, and paste the input JSON above. Use **Run Actor and get dataset** to get rows back in the same step, then feed them into an embeddings node, a Sheets node or a database. Pair it with an n8n Schedule Trigger for a recurring document sweep.

### ⚠️ Limits and caveats

Written so these do not surprise you later.

- **OCR accuracy depends on the scan, and the row tells you which you got.** On a clean 150 DPI scan expect around 95% of words read correctly. A skewed, low-resolution or heavily compressed scan does measurably worse - in our own testing a 1.5 degree skew at 110 DPI dropped a dense form to 56%, while ordinary body text held 87%. `ocrConfidence` moves with it (88 versus 60 in those two cases), so check that field before trusting the text downstream. OCR also does not reconstruct table structure.
- **Stamped scans are handled.** Court ECF headers, fax banners and Bates numbers leave a scanned page with a little real text over an image of the actual content. Those pages are OCR'd too, and the result is only kept when it is substantially richer than the stamp, so a page whose text layer was already fine is never charged for.
- **OCR is slow compared with reading a text layer.** Roughly 5-10 seconds per scanned page, against a fraction of a second for a normal page. `maxOcrPages` (default 50) keeps a single long scan from taking over the run.
- **With `ocr` off, scanned pages are flagged, not read.** You get `needsOcr: true`, `emptyTextPages` and `charCount: 0`, and you are still charged for the document, because it was opened and analysed.
- **Column detection is geometric, not perfect.** Columns are found from the whitespace gutter between them, so a page whose gutter is filled edge to edge by a wide figure or a full-width table can still fall back to reading across. Ordinary two- and three-column layouts, forms and reports come out in reading order.
- **Words broken across a line break are rejoined without the hyphen.** `informa-` plus `tion` becomes `information`, which is what a search or embedding step needs. The cost is that a genuine compound broken at exactly the line end loses its hyphen too: `position-wise` can come back as `positionwise`. Line-break hyphenation is far commoner than line-final compounds, so this is the better default, but it is a trade-off rather than a free win.
- **Tables come out as text, not as a grid.** Cell contents appear in reading order with spacing preserved. There is no table structure detection and no cell array, because a wrong table shape is worse than none.
- **With `ocr` off, only the first 5 low-text pages are probed for images.** That is what feeds `needsOcr`. A 300-page scan whose first five pages are genuinely blank covers would not be flagged. With `ocr` on the probe reaches as far as `maxOcrPages`, so the flag is exact over everything OCR was allowed to consider. `emptyTextPages` and `avgCharsPerPage` are exact for every page and are the fields to check if that edge case matters to you.
- **Encrypted files need your password.** Without a working entry in `passwords` the row comes back `errorType: "password_required"` and is not charged. There is no password cracking and none will be added.
- **Files are capped at 50 MB and 4 minutes of reading each**, and the time budget grows when OCR is on, because recognition is the slow part. A file over the size limit returns `file_too_large`. A document too long to finish inside the time budget returns the pages it read, with `truncatedAtPage` set and `processedPages` below `pageCount`, rather than failing the whole row.
- **A row's text is capped at 3,000,000 characters** by Apify's dataset item size limit. `textTruncated` tells you when that happened; `pages[]` still carries what was read.
- **`title` is often `null`, and that is the file's fault.** Many PDFs, including most LaTeX output, carry no document title. The Actor reports what the file contains and never invents a title from the first line.
- **XFA-only forms give little text.** Some form PDFs, particularly government ones, keep their content in an XFA layer. `hasAcroForm` will be `true` while the text is thin. The static page text is what comes back.
- **No login-gated files.** A URL behind a session or a paywall returns whatever the server sends an anonymous client, usually an HTML page, which is reported as `not_a_pdf` and not charged.

### Is it legal to extract text from a PDF?

This Actor downloads files from URLs you supply, using the same anonymous HTTP request a browser makes, and reads the text layer the file already contains. It does not log in, does not bypass access controls, and does not crack encryption: a protected document is opened only with a password you provide yourself.

Reading a document you are entitled to access is not in itself a legal problem, and in the US the Ninth Circuit's decision in *hiQ Labs v. LinkedIn* held that accessing publicly available pages does not violate the Computer Fraud and Abuse Act. What you do next is regulated separately. Copyright stays with whoever wrote the document, so extracted text is licensed material, not free content: quoting, redistributing or training on it may need permission. If the documents contain personal data, GDPR and similar laws apply to your processing of it. Where the files sit behind a site's terms of service, those terms govern your relationship with that site. None of this is legal advice - if your use case is redistribution or model training rather than internal analysis, talk to a lawyer.

### 🧩 Related Actors

Part of a document and public-record stack. Each one produces the links or the context this Actor turns into text.

- **[EU Tenders API](https://apify.com/webdata_labs/eu-tenders-api)** - TED procurement notices come with PDF specification documents attached. Pull the notices, then run their document URLs through this Actor to search the specifications themselves.
- **[US Building Permits API](https://apify.com/webdata_labs/us-building-permits-api)** - permit records that reference filed plan sets and notices as PDFs.
- **[SEC Financials API](https://apify.com/webdata_labs/sec-financials-api)** - EDGAR filings, where the narrative sections you actually want to read are the attached documents.
- **[Spain BOE Subastas API](https://apify.com/webdata_labs/spain-boe-subastas-api)** - auction announcements published as official PDF bulletins.
- **[Bulk URL Status Checker](https://apify.com/webdata_labs/bulk-url-status-checker)** - run a document link list through it first to drop the dead links, so every URL you pay to extract actually resolves.
- **[Website Contact Extractor](https://apify.com/webdata_labs/website-contact-extractor)** - the same sweep across a site's HTML pages, for the contact details that live outside its documents.
- **[Sanctions Screening API](https://apify.com/webdata_labs/sanctions-screening-api)** - screen the names and entities you pull out of contracts and filings against consolidated sanctions lists.

### ❓ FAQ

#### Do I need an API key or an account with anyone?

No. The Actor fetches the file over plain HTTPS and reads it locally. There is no third-party PDF service, no API key and no per-page conversion fee.

#### Am I charged for PDFs that fail?

No. A charge event fires only when a document was opened and its text delivered. Dead links, timeouts, HTML pages served instead of a file, corrupt documents and password-protected files you could not open all return a row with `status: "error"` and cost nothing.

#### Can it read scanned documents?

Yes, with `ocr` turned on. Pages that have no text layer but do carry a scan are recognised from the page image, and `ocrPages`, `ocrConfidence` and the per-page `ocrApplied` flag tell you exactly what was read and how well. Only those pages are charged for. Leave `ocr` off and the Actor simply labels them instead: `needsOcr: true` with `charCount: 0`, so you can route them elsewhere.

#### Why is `text` empty when the PDF clearly has words in it?

Check `needsOcr`. If it is `true`, the words are pixels, not text, which is the case for anything produced by a scanner or a photo - turn `ocr` on to read them. If it is `false` and the text is still empty, the file may use a broken or non-embedded font encoding; send us the URL and we will look.

#### How does it handle password-protected PDFs?

Put the candidates in `passwords`. Every encrypted file is tried against each in order and opened with the first that works, with `wasDecrypted: true` recorded on the row. Files that none of them open return `password_required` and are not charged.

#### How big and how long can a PDF be?

Up to 50 MB per file, and up to 4 minutes of reading per file. Long documents that exceed the time budget return the pages read so far with `truncatedAtPage` set, instead of failing.

#### Is the text good enough for an LLM or a vector store?

That is what the layout reconstruction is for. Lines and paragraphs survive, each page is available separately, and setting `chunkChars` returns chunks that never cross a page boundary and carry the page number for citations.

#### Does it keep the tables?

Table text comes back in reading order with spacing preserved, but there is no cell structure. If you need real tables, extract the text here and run a table parser over the page you care about.

#### Can I extract only the metadata and skip the text?

The metadata always ships. Setting `includePageText: false` drops the per-page array and keeps the whole-document `text` plus every counter, which is the smallest useful row.

#### What happens to a URL that redirects?

Redirects are followed and the landing URL is recorded in `finalUrl`, so you can see when a permalink resolved somewhere unexpected.

### 🛠️ Support

Something wrong or missing? Open an issue on the Actor's **Issues** tab with the run URL, the input you used and what you expected. Include the PDF URL when a specific file misbehaves; that is almost always enough to reproduce it.

### ⭐ Rate this Actor

If this saved you an afternoon, please leave a rating on the **Reviews** tab. Review count is the main trust signal buyers use on the Store, and the feedback decides what gets built next. If something is broken, please open an issue first so it can be fixed rather than rated low: bugs get fixed faster than they get argued about.

### Changelog

#### 0.2 - 2026-08-07

- Priced per PDF plus per page instead of a flat per-file fee, so short documents cost less and long ones pay their way.
- Multi-column pages are now segmented into blocks and read column by column instead of across the gutter. Words broken across a line break are rejoined.

#### 0.1 - 2026-08-07

- First release. Layout-aware text extraction, per-page output, document metadata with ISO 8601 dates, bookmark outline with page numbers, link annotations, scanned-document detection, password support, optional RAG chunking, and per-row error handling with no charge on failure.

# Actor input Schema

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

Direct links to the PDF files you want to read. Any public HTTP or HTTPS link works, including files stored on S3, Google Cloud Storage, an Apify key-value store or your own server. Redirects are followed. Aliases startUrls, pdfUrls, fileUrls and links are accepted too.

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

Safety cap on how many PDFs this run may process, so an oversized list cannot surprise you with a bill. Set 0 for no cap.

## `includePageText` (type: `boolean`):

Add a pages array with the text, character count, word count and dimensions of every single page, on top of the whole-document text. Turn off for smaller rows when you only need the full text.

## `includeOutline` (type: `boolean`):

Extract the PDF's table of contents as an outline array, with each entry's nesting level and the page number it points to.

## `includeLinks` (type: `boolean`):

Extract every clickable link annotation in the document, with the page each link sits on.

## `chunkChars` (type: `integer`):

Split the text into page-tagged chunks of about this many characters, breaking on paragraph boundaries and never across pages, so every chunk keeps a citable page number. Set 0 to skip chunking.

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

Read pages that have no text layer by recognising the page image. Adds the ocrApplied and ocrConfidence fields to every page and fills text for scans. OCR takes a few seconds per page, so runs over scanned files are slower than plain text extraction.

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

Language of the scanned text. Picking the right one matters: the wrong language costs accuracy on accented and non-Latin scripts. Only used when OCR is on.

## `maxOcrPages` (type: `integer`):

Safety cap on how many scanned pages may be read per file, so one 500-page scan cannot surprise you with a bill or a very long run. The row records ocrTruncated when the cap stopped it. Set 0 for no cap.

## `passwords` (type: `array`):

Optional list of passwords. Every encrypted PDF is opened with the first password that works, and the row records whether decryption was needed. Leave empty if none of your files are protected.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "maxPdfs": 100,
  "includePageText": true,
  "includeOutline": true,
  "includeLinks": true,
  "chunkChars": 0,
  "ocr": false,
  "ocrLanguage": "eng",
  "maxOcrPages": 50,
  "passwords": []
}
```

# Actor output Schema

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

One row per PDF with page count, metadata, size and the scanned-document flag.

## `scans` (type: `string`):

How much of each document had to be read by OCR, and how confident the reading was.

## `text` (type: `string`):

The full text of every PDF next to its title and page count.

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

Counts of extracted and failed PDFs, pages read, characters extracted and documents that need OCR.

# 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://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

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

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

```

## MCP server setup

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