# PDF Text Extractor - Upload & Base64 (`automa-flow/pdf-text-extractor`) Actor

Extract text and metadata from digital PDFs sent as a URL, an upload, or base64. For n8n, Make, backend and RAG pipelines that need PDF text without OCR. Extract only documents you have the right to read. This Actor does not bypass a login, paywall, CAPTCHA, or other access control.

- **URL**: https://apify.com/automa-flow/pdf-text-extractor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Developer tools, Automation, Integrations
- **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 extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PDF Text Extractor - Upload & Base64

Extract text and metadata from digital PDFs sent as URLs, uploads, or base64. Built for n8n, Make, backend and RAG pipelines that need a simple PDF-to-text step without OCR infrastructure.

Give the Actor a public PDF link, one uploaded file, or base64 from an email or webhook. Each document comes back as one Dataset row: full text, optional per-page text, page count, file size, SHA-256, basic metadata and an explicit status. The Console prefill downloads one short public sample, [W3C's dummy PDF](https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf), so the first run has a row to inspect.

A second run is the next batch. Point it at the next file that arrived. This Actor does not watch a URL for changes.

Latest notes are in the [changelog](https://apify.com/automa-flow/pdf-text-extractor/changelog).

### Why use it

Automation builders already receive PDFs as attachments and links. The job is to turn a digital PDF that already has a text layer into structured text for an LLM, a CRM, a sheet or a vector pipeline.

This Actor is the small version of that step. It accepts an upload and base64 as well as a URL, and it stops when the file has no text layer. It does not reconstruct tables, rewrite the text as Markdown, or call an OCR engine.

### Supported input

Documents are read in this order: `urls`, then `base64Pdfs`, then `pdfFile`. One run can mix them, up to 200 documents. A base64 batch is limited to 32 files so the input fits in the default 1024 MB. A larger batch should be public URLs or the single upload.

| Input | Use it when |
| --- | --- |
| `urls` | The PDF is already on a public http or https URL. |
| `pdfFile` | You upload one PDF in the Console, or the platform gives you the uploaded-file URL. |
| `base64Pdfs` | A webhook, n8n, Make, or your backend has the PDF bytes. A `data:application/pdf;base64,` prefix is accepted. |

To send only an upload or base64, omit `urls` or set it to `[]`. If the platform fills `urls` with the sample and the request also has `base64Pdfs` or `pdfFile`, the sample is skipped. `RUN_SUMMARY` then sets `droppedDefaultSampleUrl`. The sample still runs when it is the only input.

Private hosts, localhost, link-local addresses, cloud metadata hosts and URLs with embedded passwords are rejected. Ports other than 80 and 443 are rejected.

### Quick start

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "includePages": true,
  "maxFileSizeMb": 5,
  "maxPagesPerPdf": 500
}
```

Open the run's Dataset. A digital sample returns `status: "SUCCESS"` and the page text. Check `RUN_SUMMARY` in the Key-Value Store for counts, the charge total and the next step.

### Examples

Three saved examples follow the jobs above. Replace the sample, then read the result table.

One public PDF starts from the W3C dummy file. The table shows the file name, page count and text. A scan is not listed in that table: its status is `NO_TEXT_LAYER`.

A list of PDFs starts from three arXiv papers. Each file has its own status, so one bad link stays in the list.

A base64 PDF is the input for n8n, Make, or a webhook. The sample is one page, and its text is "Digital PDF for the upload check." Replace the file name and the payload.

### Sample output

The row below shows the Dataset contract. Hashes and timestamps are illustrative. A live run replaces them from the file it read. Query values in `source_url` are redacted, and a short hash is appended.

```json
{
  "record_type": "PDF_RESULT",
  "status": "SUCCESS",
  "input_type": "URL",
  "input_index": 0,
  "source_url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
  "file_name": "dummy.pdf",
  "sha256": null,
  "file_size_bytes": 13264,
  "page_count": 1,
  "extracted_page_count": 1,
  "text": "Dummy PDF file",
  "pages": [
    {"page": 1, "text": "Dummy PDF file"}
  ],
  "metadata": {
    "title": null,
    "author": null,
    "subject": null,
    "creator": null,
    "producer": null,
    "creation_date": null,
    "modification_date": null
  },
  "character_count": 14,
  "word_count": 3,
  "error": null,
  "duplicate_of": null,
  "charged": true,
  "source": "user_pdf",
  "source_id": "0000000000000000000000000000000000000000000000000000000000000000",
  "schema_version": 1,
  "processed_at": "2026-09-23T00:00:00Z",
  "scraped_at": "2026-09-23T00:00:00Z",
  "fingerprint": "1111111111111111111111111111111111111111111111111111111111111111"
}
```

`text` is null when the row is not a successful extraction. An empty string is not used as a stand-in for success.

### n8n, Make and backend

Put the PDF bytes in base64 and start a run with your own Apify token. The body below is the Actor input. Replace the data with your payload.

```json
{
  "urls": [],
  "base64Pdfs": [
    {
      "filename": "attachment.pdf",
      "data": "<base64>"
    }
  ],
  "includePages": true
}
```

```text
POST https://api.apify.com/v2/acts/automa-flow~pdf-text-extractor/runs
Authorization: Bearer <your Apify token>
Content-Type: application/json
```

In n8n or Make, map the attachment's base64 into `base64Pdfs[0].data`. When the run finishes, read the Dataset items. Keep the row when `status` is `SUCCESS`. Route `NO_TEXT_LAYER` to an OCR step of your own. This Actor will not do that step for you.

Before the call, set `maxTotalChargeUsd` to at least `0.01 + 0.003 * PDFs you expect to succeed` at the default 1 GB memory.

### Batch example

```json
{
  "urls": [
    "https://example.com/report-a.pdf",
    "https://example.com/report-b.pdf"
  ],
  "base64Pdfs": [
    {
      "filename": "notes.pdf",
      "data": "<base64>"
    }
  ],
  "includePages": false,
  "maxFileSizeMb": 5,
  "maxPagesPerPdf": 500
}
```

`includePages: false` still returns the full `text`. It omits the per-page array so a large batch stays smaller. One bad file does not drop the others. Identical bytes in the same run are extracted once: the later row is `DUPLICATE` and is not charged again.

### Statuses

| Status | Meaning |
| --- | --- |
| `SUCCESS` | Text was extracted. This is the only status that can incur `pdf-extracted`. |
| `DUPLICATE` | Same SHA-256 as an earlier document in this run. Not parsed or charged again. |
| `NO_TEXT_LAYER` | The file opened and no page had extractable text. OCR was not run. |
| `INVALID_PDF` | The bytes are not a readable PDF. |
| `PASSWORD_PROTECTED` | The PDF asks for a password. Unlocking is not supported. |
| `TOO_LARGE` | The file is over `maxFileSizeMb`, or the run reached the 1 GB decoded-byte ceiling. |
| `TOO_MANY_PAGES` | The PDF has more pages than `maxPagesPerPdf`. |
| `DOWNLOAD_FAILED` | The URL did not return the file after the retry budget. |
| `INVALID_URL` | The URL is not a public http(s) address on port 80 or 443. |
| `FAILED` | The parser, the 2,000,000 character text cap, or the spending limit stopped this document. `error` says which. |

`NO_TEXT_LAYER` is a completed observation. It is not a failed download and it is not charged. A whole run fails only for invalid input, a pricing or billing state that cannot be resolved safely, or a failure to persist output. A batch of bad PDFs still finishes with one row per input.

`RUN_SUMMARY` lists the counts, whether the spending limit stopped the run, and `nextAction`.

### Pricing

Pay per event. At the default 1 GB of memory:

| Event | Price |
| --- | --- |
| Actor start | $0.01 per run (one event per GB of memory, minimum one) |
| `pdf-extracted` | $0.003 per unique PDF with `SUCCESS` |

At the default 1 GB, one successful PDF is $0.013, ten are $0.04, and a full run of 200 unique successes is $0.61. Those totals are the $0.01 start plus $0.003 for each success. Platform usage (compute, data transfer, storage) is billed separately by Apify.

There is no `pdf-extracted` charge for retries, duplicate documents, failed downloads, invalid PDFs, password-protected PDFs, files over the size, page or text limit, or `NO_TEXT_LAYER`. The Actor start charge and platform usage still apply.

Set `maxTotalChargeUsd` before an API or agent run. A successful PDF is saved and charged in the same Dataset write. If the limit refuses that charge, the success row is not saved. Later documents that were not already extracted return `FAILED` with no text. A later copy of a PDF already extracted in the run stays `DUPLICATE` and is not charged again. Rows already saved stay saved. Raise the limit and run the refused inputs again.

This is a flat event price. It is not a claim to be the lowest price on the Store.

### Limits

| Limit | Value |
| --- | --- |
| Documents per run | 200 total across all inputs, including at most 32 base64 files and one upload |
| File size | 1 to 5 MB (`maxFileSizeMb`) |
| Pages per PDF | 1 to 500 (`maxPagesPerPdf`) |
| Decoded bytes per run | 1 GiB guard. 200 URL files at 5 MiB each are 1000 MiB, so the file-size and count limits bind first. Base64 is capped at 32 files so the input strings fit in the default 1024 MB. |
| Extracted text | 2,000,000 characters. A larger extraction returns `FAILED` and is not charged. |
| Dataset item | About 8.5 MB of UTF-8. Page text is dropped first. If the row is still too large, that document is `FAILED` and is not charged. |
| Download attempts | 3, for DNS failures, network errors, HTTP 429 and HTTP 5xx. Each attempt has a 60 second limit, including a slow drip of bytes. Exceeding that limit is `DOWNLOAD_FAILED` and is not retried. The wait between retries is cut to the time left before the run deadline. |
| Accepted response | HTTP 200 without HTTP content compression. The Actor requests `Accept-Encoding: identity`; gzip, deflate and Brotli responses are rejected as `DOWNLOAD_FAILED`. Compression inside the PDF is supported. |
| Parser time | 90 seconds per document, or the time left before the run deadline, whichever is shorter. The parser runs in a separate process. A timeout stops that process, returns `FAILED` for that file, and the next document starts in a new process. It is not charged. |
| Run deadline | New work stops about 30 seconds before the Actor timeout to save remaining statuses and `RUN_SUMMARY`. Retry waits and parser time use only the time that remains. If the Dataset cannot confirm all remaining rows before the finalization deadline, the run fails with `SYSTEM_FAILURE` and preserves confirmed results. |
| Redirects | 5, and each target is checked again |
| Memory | 1024 MB default. Actor start scales with memory above that. |

There is no concurrency input. Downloads use a small internal pool. Parsing runs one document at a time so a batch stays within the memory limit.

If `RUN_SUMMARY.finalizationIncomplete` is true, `inputs` still counts every submitted document and `datasetRows` counts confirmed writes. Check `unconfirmedInputIndices` against the Dataset before retrying. `uncertainWriteInputIndices` identifies a write whose confirmation was lost, so its rows may already exist. The Actor does not replay those writes automatically.

### Privacy and security

Use this Actor only for documents you have the right to extract. Do not use it to bypass a login, a paywall, a CAPTCHA, or any other access control.

These files may contain confidential or personal information. Extraction runs inside the Actor. Bytes are not sent to an external PDF, OCR, or LLM API. The Actor log does not include document text, base64, or uploaded bytes. Signed URL query values are redacted before a row is stored.

The Dataset and the run input live in your Apify storage. How long they remain follows your Apify account and storage settings. This page does not claim a GDPR or HIPAA certification.

Only public http and https URLs are fetched. Local, private, link-local and cloud-metadata addresses are blocked, including after redirects. A public URL can still point at a file the caller is not allowed to copy. That residual risk is moderate: private hosts are blocked, and the Actor does not decide whether a reachable public file may be extracted.

### API and MCP

After Store publication, agents can discover the Actor through Apify MCP search. Running it uses your own Apify account and token. A private Actor requires authenticated direct access.

Direct MCP endpoint:

```text
https://mcp.apify.com?tools=automa-flow/pdf-text-extractor
```

When an agent should call it: you have a digital PDF as a URL, an upload, or base64, and you need the text plus a status before writing a record, a chunk, or a sheet row.

When an agent should not call it: the PDF is a scan, you need OCR, table reconstruction, Markdown layout, or a password removed.

Ask it to extract the text and return the status, page count and text. Then read `RUN_SUMMARY` before fetching a large Dataset. If `chargeLimitReached` is true, raise `maxTotalChargeUsd` and rerun the documents that were not charged.

The output links are:

- Dataset items: `results` in the run output
- Summary: Key-Value Store record `RUN_SUMMARY`
- Charges: Key-Value Store record `BILLING_RECEIPT`

### Technical details

Text comes from the PDF text layer through PyMuPDF, on the Actor itself. The full `text` value is the page texts joined with a newline after CR LF and CR are turned into LF. NUL bytes are removed so the row can be stored as JSON. Other characters are left as the extractor returned them. The Actor does not claim that the text preserves visual layout.

Each acquired file is hashed with SHA-256. The same hash later in the run reuses the first result. `source_id` is that hash when the bytes were read. `fingerprint` hashes the semantic fields and leaves out the clock and the charged flag, so a later batch can be compared.

The receipt records the document hash before the charged Dataset write. The success row and the `pdf-extracted` event are one write: if the spending limit refuses the event, that row is not stored. If the charge state cannot be resolved, the run stops and does not charge that document again on a replay.

# Changelog

This Actor's version history is a separate document: https://apify.com/automa-flow/pdf-text-extractor/changelog.md

# Actor input Schema

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

Public http:// or https:// PDF URLs. Leave this an empty array when you only upload a file or send base64. The Console prefill is one short public sample PDF.

## `base64Pdfs` (type: `array`):

PDF files as standard base64, or as data:application/pdf;base64,... URLs. Each item needs a filename and the payload. At most 32 files, so the batch fits in the default 1024 MB. A larger batch should be public URLs or the single upload.

## `pdfFile` (type: `string`):

One PDF file. On Apify this is the uploaded-file URL. Omit it when you are not uploading.

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

When true, each SUCCESS row includes a pages array. Turn it off to keep large runs smaller.

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

Reject a document larger than this size. The ceiling is 5 MB until Cloud cost measurements say otherwise.

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

Reject a PDF with more pages than this. The ceiling is 500 until Cloud cost measurements say otherwise.

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "includePages": true,
  "maxFileSizeMb": 5,
  "maxPagesPerPdf": 500
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

## `billingReceipt` (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 = {
    "urls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ],
    "includePages": true,
    "maxFileSizeMb": 5,
    "maxPagesPerPdf": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/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"],
    "includePages": True,
    "maxFileSizeMb": 5,
    "maxPagesPerPdf": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/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"
  ],
  "includePages": true,
  "maxFileSizeMb": 5,
  "maxPagesPerPdf": 500
}' |
apify call automa-flow/pdf-text-extractor --silent --output-dataset

```

## MCP server setup

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