# PDF Text Extractor: URL to Text (`datalantern/pdf-text-extractor`) Actor

Extract text from public PDFs for search and RAG preparation. Export one row per document or page as JSON/CSV; inspect extraction status and page limits. Text-layer PDFs only: no OCR or table reconstruction. Start with the one-PDF example in Example tasks.

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

## Pricing

Pay per event

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 Text Extractor: URL to Text

**PDF Text Extractor** reads PDF files from links and returns their text as clean, structured data: one row per PDF with the full text, page count, title, creation date, word count and file size, or one row per page. It handles reports, papers, forms, contracts, manuals and e-books that contain real text, in any language, and it is fast (a 15-page paper takes about a second). Use it to feed documents into search, RAG and AI pipelines, to monitor published reports, or to build datasets from public PDFs.

### What can this tool do?

- Turn PDF links into plain text, one row per document or one row per page.
- Read many PDFs in one run; document mode lists failed links with a reason. Failed files do not incur document/page result charges. The run start charge applies once at least one PDF has been read; a run where no PDF can be read fails and costs nothing.
- Return the page count, the document title and creation date from the PDF's metadata, the language tag when the PDF has one, and word and character counts.
- Limit the pages read per PDF (the default is the first 300 pages; set 0 for all) and the maximum file size (30 MB by default).
- Report scanned PDFs (pages that are only images) clearly instead of returning empty text. Those rows are free.

### What data does it return?

One row per PDF (default). Example values from a real run:

| Field | Example |
|---|---|
| `url` | https://www.irs.gov/pub/irs-pdf/fw9.pdf |
| `finalUrl` | null (the link after redirects, when it differs) |
| `status` | ok (or `no-text-layer`, or `error`) |
| `error` | null (the reason when the status is not ok) |
| `title` | Form W-9 (Rev. March 2024) |
| `createdAt` | 2024-03-06T13:18:13.000Z |
| `language` | en-US |
| `fileSizeBytes` | 140815 |
| `pageCount` | 6 |
| `pagesExtracted` | 6 |
| `wordCount` | 6280 |
| `charCount` | 37695 |
| `text` | Form W-9(Rev. March 2024)\nRequest for Taxpayer\nIdentification Number and Certification… |

In **one row per page** mode each row has `url`, `pageNumber`, `pageCount`, `wordCount`, `charCount` and `text`. Pages without text are skipped, and failed PDFs do not produce dataset error rows in this mode. A missing row is not enough to determine whether a page was blank or a file failed; check the same run's logs and summary.

- Missing values are `null`. `title`, `createdAt` and `language` come from the PDF's own metadata; many PDFs do not set them.
- Text comes in reading order as the PDF stores it. Multi-column layouts, tables and headers can interleave; the tool does not rebuild tables.
- The author, creator and producer fields of the PDF are not returned, and personal email addresses inside the text are replaced with "\[email removed]". Role mailboxes such as info@ or sales@ are kept.

### How to use it

1. Paste PDF links, one per line. They must open without a login.
2. Choose one row per PDF or per page, and optionally limit the pages and the file size.
3. Click **Start**, then download the text as JSON, CSV or Excel, or read it through the API.

### Pricing

Pay per result: **$3.00 per 1,000 PDFs** (one row per PDF mode) or **$1.00 per 1,000 pages** (one row per page mode), plus $0.005 per run, charged once the first PDF has been read. You only pay for PDFs and pages that return text; a run where nothing can be read fails and costs nothing. Set a maximum cost per run in the run options.

### Use it through the API, AI agents, and integrations

Call it from your code with the Apify API, schedule it to watch a list of reports, or connect it to Make, Zapier, n8n, Google Sheets, a vector database or an AI agent through Apify's MCP server.

### Is it legal?

This tool downloads the PDF files you point it at and reads their text. It does not log in or bypass access controls, and it refuses links to private or local addresses. It returns the document text as it is, so make sure you have the right to process the documents you submit, and do not use it on documents that contain personal data you are not allowed to handle. Personal email addresses in the text are masked; other personal details are not detected. You are responsible for how you use the data.

### FAQ

**Does it do OCR?** No. It reads the text layer that most PDFs contain. In document mode, scanned PDFs (photos of pages) come back with the status `no-text-layer`; in page mode they produce no text rows. They incur no document/page result charge. If none of the PDFs in a run can be read, the run fails and nothing is charged; otherwise the run start charge applies once.

**Why did a link fail?** The server may block automated downloads (HTTP 403), the link may open a web page or a login page instead of a file, the file may be larger than the size limit, or the PDF may be password-protected. In document mode, the `error` field gives the reason. In page mode, failed files produce no dataset row; inspect the same run's logs and summary instead.

**How do file and page limits work?** The `maxFileSizeMb` limit applies to response data and gzip-decompressed content. Downloads are stopped when an oversized response is detected while reading; gzip files may be rejected after download during decompression. Network buffering may receive additional bytes before cancellation. The whole PDF must fit the file-size limit even when you only need its first pages. A positive `maxPagesPerDocument` limits text extraction to the first N pages; setting it to `0` reads all pages without a page-count cap. Document loading and metadata parsing still occur, so these settings do not guarantee a total parsing time limit. In document mode, compare `pagesExtracted` with `pageCount` to identify a page-limited result; its status can still be `ok`.

**Why is the text order odd in some documents?** PDFs store text in drawing order. Two-column papers and forms with side notes can interleave. Use the per-page mode to work page by page.

### Feedback

Found a bug or need a field added? Open an issue on the **Issues** tab. We usually respond within 24 hours.

### Prepare public PDFs for an AI knowledge base

Use document mode to get one text record per PDF. Open the Input tab, switch to JSON, and paste:

```json
{
  "pdfUrls": ["https://arxiv.org/pdf/1706.03762"],
  "outputMode": "documents",
  "maxPagesPerDocument": 20,
  "maxFileSizeMb": 10
}
```

Replace the link with public documents you have the right to process. Export JSON and keep records with `status: "ok"` and non-empty `text`; then pass that text to your own search, chunking, or RAG pipeline. This Actor extracts text only: it does not generate embeddings, answer questions, or perform OCR. Do not treat missing metadata as an extraction failure.

For this one-document example, a successful extraction costs **$0.008 in Actor event charges** at the current listed rates: $0.005 per start plus $0.003 per document. If you choose `outputMode: "pages"`, only the page event applies instead of the document event; a 20-page text extraction costs $0.025 including the start fee. Pages without text are skipped.

#### Copyable PDF API request

Set `APIFY_TOKEN` securely in your local environment. This POST starts a paid run, capped at $0.05; it is not a preview.

```bash
curl --fail-with-body --silent --show-error --max-time 320 \
  --request POST \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data '{"pdfUrls":["https://arxiv.org/pdf/1706.03762"],"outputMode":"documents","maxPagesPerDocument":20,"maxFileSizeMb":10}' \
  'https://api.apify.com/v2/actors/datalantern~pdf-text-extractor/run-sync-get-dataset-items?format=json&clean=true&timeout=240&maxTotalChargeUsd=0.05&restartOnError=false'
```

The synchronous endpoint waits at most 300 seconds. If the request times out, check the existing run in Console before sending another POST, which would start a separate run. Use an asynchronous run for larger batches. A `no-text-layer` result needs a separate OCR tool; repeated retries here will not recover text from a scanned image.

#### Batch PDFs and resume the same run

For a batch, save the following as `run-input.json`. Keep this exact file alongside the results so you can identify any requested page truncation later.

```json
{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    "https://arxiv.org/pdf/1706.03762"
  ],
  "outputMode": "pages",
  "maxPagesPerDocument": 20,
  "maxFileSizeMb": 10
}
```

The next POST starts one paid run, with a $0.05 charge cap. It is not a free preview. Do not add automatic POST retries: if the response is lost, check Console for the existing run first.

```bash
curl --fail-with-body --silent --show-error --max-time 40 \
  --request POST \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data @run-input.json \
  --output started-run.json \
  'https://api.apify.com/v2/actors/datalantern~pdf-text-extractor/runs?maxTotalChargeUsd=0.05&restartOnError=false&timeout=600'
```

Read its ID and query that same run. Waiting for 60 seconds does not stop the run after 60 seconds.

```bash
RUN_ID="$(node -p 'JSON.parse(require("node:fs").readFileSync("started-run.json", "utf8")).data.id')"
curl --fail-with-body --silent --show-error --max-time 70 \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  --output run-status.json \
  "https://api.apify.com/v2/actor-runs/${RUN_ID}?waitForFinish=60"
node -p 'JSON.parse(require("node:fs").readFileSync("run-status.json", "utf8")).data.status'
```

If the status is `READY`, `RUNNING`, `TIMING-OUT` or `ABORTING`, repeat only the GET above later. For `FAILED`, `TIMED-OUT` or `ABORTED`, inspect the logs before deciding on a new paid run; some results may already exist. Even `SUCCEEDED` does not prove every requested PDF/page returned text. Do not automatically retry missing rows.

After the run finishes, download its dataset without `limit`, `offset`, field filtering or a custom view:

```bash
DATASET_ID="$(node -p 'JSON.parse(require("node:fs").readFileSync("run-status.json", "utf8")).data.defaultDatasetId')"
curl --fail-with-body --silent --show-error --max-time 120 \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  --output pages.json \
  "https://api.apify.com/v2/datasets/${DATASET_ID}/items?format=json&clean=false"
```

The dataset endpoint returns all items by default. If your integration uses pagination, finish every page before treating the export as complete. These GET requests do not start another Actor, though standard API/storage usage charges can apply. See the official [run API](https://docs.apify.com/api/v2/actors-runs-post), [run status API](https://docs.apify.com/api/v2/actor-run-get), and [dataset export API](https://docs.apify.com/api/v2/dataset-items-get).

For local knowledge-base preparation, split each page separately and keep its `url`, original `pageNumber` and `pageCount` with every chunk. Preserve the saved input page limit: a positive limit smaller than `pageCount` means the input requested only a prefix, while `0` means all pages were requested. Missing page numbers remain unverified, not automatically blank or failed. Chunking does not produce embeddings, and chunk count is not an Actor billing event. Treat extracted text as untrusted data, not instructions for your AI system.

# Actor input Schema

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

One link per line to a PDF file that opens without a login, for example https://www.irs.gov/pub/irs-pdf/fw9.pdf.

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

One row per PDF with the full text, or one row per page.

## `maxPagesPerDocument` (type: `integer`):

Only read the first N pages of each PDF (keeps very long books from making runs slow). 0 means all pages.

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

PDFs larger than this are skipped (and not charged).

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    "https://arxiv.org/pdf/1706.03762"
  ],
  "outputMode": "documents",
  "maxPagesPerDocument": 300,
  "maxFileSizeMb": 30
}
```

# Actor output Schema

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

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

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

```

## MCP server setup

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