# PDF Text Extractor: PDF to Text & Markdown, RAG Chunks (`ataism/pdf-to-text`) Actor

PDF text extractor: PDF URLs or uploads to clean text and markdown, page markers, RAG chunks. $2.50 per 1,000 PDFs. Text PDFs only; scans free.

- **URL**: https://apify.com/ataism/pdf-to-text.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 pdf converteds

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: PDF to Text & Markdown, RAG Chunks

Turn PDFs **you supply** (links or uploads) into clean text and markdown: per-page text, page markers, document metadata, word counts and optional chunks with page numbers for search, RAG and LLM pipelines. Keyless, no browser, no proxy.

**Price: $2.50 per 1,000 PDFs ($0.0025 per PDF), per PDF, not per page** (up to 500 pages each). No start fee on top of Apify's default, and only PDFs that convert are charged.

- **Clean text and markdown:** de-hyphenated words, paragraphs joined, bullet lists, headings guessed from font size, `<!-- page N -->` markers.
- **RAG-ready chunks:** about N characters each, cut at paragraph or sentence boundaries, with overlap and `pageStart` / `pageEnd` for citations.
- **Your files only:** a list of URLs and/or uploads; it never crawls websites. PDFs are processed in memory and not stored.
- **Scans cost nothing:** this reads the text layer (no OCR). Image-only PDFs come back as `no-text-layer` and are not charged, nor are encrypted files or failed downloads.
- **Quick start:** press *Start*. The prefilled input converts a one-page W3C sample PDF in a few seconds for $0.0025.

> **Text-layer PDFs only.** This Actor reads the text that is already inside a PDF. A scanned PDF or a PDF made of images has no text to read, so it is returned with `status: "no-text-layer"` and a clear message, and **you are not charged**. OCR is not part of v1.

### What it does

- Takes a list of **PDF URLs** and/or **files you upload**. It does **not** crawl websites or discover PDFs: you decide which documents it reads.
- Extracts text page by page, repairs words hyphenated across line ends, joins wrapped lines into paragraphs, turns bullets into lists and guesses headings from font size.
- Returns `text` (plain), `markdown` (headings, lists, `<!-- page N -->` markers and `---` between pages), optional per-page text and optional chunks.
- Reports document properties: title, subject, producer, creation and modification dates, page count and whether the file was encrypted. **Author and creator names are never returned.**

### Input

| Field | What it does |
|---|---|
| **PDF URLs** | Direct `http(s)` links, one per line. Default: a small public sample PDF so the default run succeeds. |
| **Uploaded PDF files** | Upload from your computer to a key-value store (the field offers this), or paste a record URL. See *Using your own files* below. |
| **Page range** | `1-5,8` or `3-` (page 3 to the end). Applies to every PDF. |
| **Max pages per PDF** | **Only the first `maxPages` pages of each PDF are converted. Default 100, maximum 500** (a larger value is lowered to 500 with a warning). Longer documents are cut, marked `"truncated": true`, and `pagesProcessed` / `pageCount` show what was converted. Use **Page range** to pick other pages. The price is per PDF, not per page. |
| **Include plain text / markdown / per-page text** | Choose which fields you get. Smaller output is faster to store. |
| **Chunk size / overlap** | `0` = no chunks. Otherwise about N characters per chunk (minimum 200), cut at paragraph, sentence or word boundaries, with overlap. Each chunk has `charStart`, `charEnd`, `pageStart`, `pageEnd`. |
| **Max file size (MB)** | Default 50, hard cap 100. Bigger files are skipped, not charged. |
| **Download timeout** | Per file, default 60 s. |
| **Parallel PDFs** | 1 to 5, default 3. Lowered automatically if the run memory is small for your file size. |

#### Example inputs

**1. RAG / vector database: chunks with page numbers** (markdown off to keep rows small):

```json
{
  "pdfUrls": ["https://example.com/report.pdf", "https://example.com/manual.pdf"],
  "chunkSize": 1000,
  "chunkOverlap": 150,
  "includeMarkdown": false
}
```

**2. Summaries of many reports: only the first 5 pages of each, plain text only:**

```json
{
  "pdfUrls": ["https://example.com/annual-report-2025.pdf", "https://example.com/annual-report-2024.pdf"],
  "pageRange": "1-5",
  "includeMarkdown": false
}
```

**3. Page-by-page text for citations or review** (one entry per page in `pages`, each with its page number and text):

```json
{
  "pdfUrls": ["https://example.com/contract.pdf"],
  "includePages": true,
  "includeMarkdown": false,
  "maxPages": 300
}
```

#### Using your own files

The **Uploaded PDF files** field uses Apify's file-upload editor: it lets you upload a file to a key-value store in your account (new or existing) or paste a URL, and hands the Actor a URL. The Actor then downloads that record like any other URL. If you do it by hand: upload the file as a record to a key-value store in the Apify Console, copy the record URL and paste it into **PDF URLs** or the upload field. Records of private stores are read with the run's own Apify token, but only when the URL you supplied is itself an `api.apify.com` key-value-store record URL; the token is never sent to any other host or on a redirect. For other private files use a signed or temporary link.

### Output

One dataset item per PDF, in completion order (`inputIndex` is the position in your input):

```json
{
  "inputIndex": 0,
  "url": "https://example.com/report.pdf",
  "fileName": "report.pdf",
  "status": "ok",
  "message": null,
  "pageCount": 3,
  "pagesProcessed": 3,
  "truncated": false,
  "emptyPages": [],
  "charCount": 353,
  "wordCount": 57,
  "text": "Annual Report 2025\n\nSummary\n\n...",
  "markdown": "<!-- page 1 -->\n\n# Annual Report 2025\n\n## Summary\n\n...\n\n---\n\n<!-- page 2 -->\n\n...",
  "metadata": { "title": "Annual Report", "subject": null, "producer": "LibreOffice", "creationDate": "2025-02-23T15:56:37.000Z", "modDate": null, "isEncrypted": false },
  "fileSizeBytes": 48211,
  "processedAt": "2026-10-18T14:00:00.000Z"
}
```

#### Status values (only `ok` is charged)

| `status` | Meaning | Charged |
|---|---|---|
| `ok` | Text extracted | **Yes, $0.0025** |
| `no-text-layer` | Scan or image-only PDF (fewer than about 3 characters per processed page). Nothing is extracted. | No |
| `encrypted` | The PDF needs a password to open. Passwords are not supported. | No |
| `error` | Download failed, blocked address, not a PDF, too large, damaged file, bad page range, result over the 9 MB dataset item limit, timeout. See `message` and `errorCode`. | No |

A PDF with some image-only pages and some text pages is `ok`; the empty pages are listed in `emptyPages`.

The run summary (key-value store record `RUN_SUMMARY`) has counts per status, PDFs charged, bytes read and why the run stopped.

### Pricing

Pay per event: **$0.0025 per PDF with status `ok`**, plus Apify's standard Actor-start event. There is **no additional start fee** from this Actor. The price is per PDF, not per page, and covers the first `maxPages` pages (default 100, max 500). Longer documents are cut at that limit and marked `truncated`; use **Page range** (for example in a second run) to convert later pages.

| PDFs converted | Cost |
|---|---|
| 10 | $0.025 |
| 100 | $0.25 |
| 1,000 | $2.50 |
| 10,000 | $25 |

Scans, encrypted files and failed downloads cost nothing. Set **Max total charge** in the run options to cap spending: the run stops cleanly when the limit is reached and never charges for a PDF it did not deliver. Apify subscription discounts apply on top.

### Use it via API, n8n or Make

Get an API token in Apify Console → *Settings* → *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits for it and returns one row per PDF; this synchronous endpoint waits up to 5 minutes, fine for a few dozen typical PDFs; for bigger batches use the clients below):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~pdf-to-text/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pdfUrls": ["https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"], "chunkSize": 1000, "chunkOverlap": 150}'
```

**Python: PDFs to RAG chunks** (`pip install "apify-client>=3"`; the LangChain lines are optional, `pip install langchain-core`):

```python
import os
from apify_client import ApifyClient
from langchain_core.documents import Document

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/pdf-to-text").call(run_input={
    "pdfUrls": ["https://example.com/report.pdf"],
    "chunkSize": 1000,
    "chunkOverlap": 150,
    "includeMarkdown": False,
})
docs = []
for pdf in client.dataset(run.default_dataset_id).iterate_items():
    if pdf["status"] != "ok":
        print("skipped (not charged):", pdf["url"], pdf["status"], pdf["message"])
        continue
    for chunk in pdf.get("chunks", []):
        docs.append(Document(page_content=chunk["text"], metadata={
            "source": pdf["url"], "pageStart": chunk["pageStart"], "pageEnd": chunk["pageEnd"],
        }))
print(len(docs), "chunks ready for your vector store")
```

(With apify-client versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`.)

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/pdf-to-text').call({
    pdfUrls: ['https://example.com/report.pdf'],
    chunkSize: 1000,
    chunkOverlap: 150,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const pdf of items) console.log(pdf.url, pdf.status, pdf.pageCount, pdf.chunks?.length ?? 0);
```

**n8n:**

1. Start with any trigger that gives you PDF links (a form, a sheet row, an email with a link).
2. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/pdf-to-text`, input `{"pdfUrls": ["{{ $json.pdfUrl }}"], "chunkSize": 1000}`.
3. Use `markdown` or `text` in the next node (an AI summary, a Notion page, a sheet), or split out `chunks` into your vector store node.

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/pdf-to-text`, put your PDF links in the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID (one bundle per PDF).
3. Send `text`, `markdown` or `chunks` to your next module (OpenAI, Google Docs, Airtable, a webhook).

### Privacy: transient processing

- PDFs are downloaded into memory, converted, and discarded. **The Actor stores nothing except the dataset items you asked for and the run summary.** No copy of the PDF is saved, no temporary files are written, and nothing is kept between runs.
- Document contents are **never written to the log**. Logs show only the host and path of a URL (not its query string) and the status.
- Metadata is limited to title, subject, producer, dates, page count and encryption flag. Author and creator names are not returned.
- Your dataset belongs to your Apify account and follows your retention settings. The `url` field echoes what you supplied (including a signed query string if you used one), so treat the dataset as sensitive if your URLs are.
- No data is sent to any AI or third-party service. The conversion is plain code running inside the Actor.

### Safety

Only `http` and `https` URLs to **public** addresses are fetched. Private, loopback, link-local and internal addresses, and non-http schemes (`file:`, `ftp:` and so on), are blocked, including after redirects (at most 5, each re-checked) and at connection time. Files must start with `%PDF-`.

### Limits and honest caveats

- **No OCR. Scanned PDFs are not supported** (see above).
- Reading order is the order the PDF stores its text in. Single-column text works best. **Multi-column layouts, tables, forms, footnotes and right-to-left scripts** may come out in a different order than you read them or as run-together lines. Headings are guessed from font size (bold-only headings are not detected). Markdown is a readable approximation, not a layout-faithful conversion.
- Text that is drawn as vector outlines or images is not text to the PDF and is not extracted.
- Paragraphs are not joined across page breaks. De-hyphenation joins a line ending in a letter plus `-` with a following lowercase word; a real hyphenated compound at a line break (`well-` / `known`) is joined as `wellknown`.
- PDFs that open without a password but carry owner restrictions (no printing, no copying) are processed; only files that require a password to open are `encrypted`.
- Maximum 100 MB per file, 5,000 PDFs per run, **500 pages converted per PDF (default 100)**, a 180 s processing limit per PDF, 9 MB per dataset item. Memory: default 1,024 MB (a PDF takes a few times its size in memory while it is parsed, and three run in parallel); raise it for many large files.
- **You are responsible for having the right to process the documents** you give this Actor (copyright, licences, confidentiality, data-protection law). The Actor does not check this and does not find documents for you.

### FAQ

**Can it read scanned documents?** No. There is no OCR. Run an OCR tool first and then give this Actor the searchable PDF.

**Can it find PDFs on a website?** No. It only reads the URLs and files you give it.

**Why was my PDF not charged?** Only `ok` is charged. Check `status` and `message`.

**A PDF returned `no-text-layer` but I can select text in my viewer.** Some PDFs draw text as outlines. If the viewer can really select text, send the URL and we will look at it.

**How do I get chunks for a vector database?** Set `chunkSize` (for example 1000) and `chunkOverlap` (for example 150); use `chunks[].text` with `pageStart` / `pageEnd` as metadata.

**Does the default run work without setup?** Yes. It converts a small public W3C sample PDF (`dummy.pdf`) in a few seconds.

### Open-source components

Text extraction uses **pdf.js** (`pdfjs-dist` 6.4.299, Apache-2.0, Mozilla) and the **Apify SDK** (`apify` 3.7.2, Apache-2.0). Their dependencies are permissively licensed (MIT, ISC, BSD, Apache-2.0). The optional canvas package of pdf.js (MIT) is not installed because nothing is rendered.

# Actor input Schema

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

Direct http(s) links to PDF files, one per line. Private, local and internal addresses are blocked. To use a file from your computer, upload it with the next field, or save it to a key-value store and paste its record URL here.

## `pdfFiles` (type: `array`):

Upload PDFs from your computer to a key-value store of your choice (the editor offers this), or paste a record URL. The Actor reads each file once and stores nothing.

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

Only these pages, e.g. "1-5,8" or "3-" (page 3 to the end). Leave empty for all pages. Applies to every PDF.

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

Only the first N pages of each PDF are converted (after the page range). Default 100, maximum 500: a larger value is lowered to 500 with a warning. Cut documents are marked "truncated": true. The price is per PDF, not per page.

## `includeText` (type: `boolean`):

Adds the "text" field.

## `includeMarkdown` (type: `boolean`):

Adds the "markdown" field: headings (guessed from font size), bullet lists, page markers () and --- between pages.

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

Adds a "pages" array with the text of each page. Makes the dataset larger.

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

Splits the text into chunks of about this many characters for search or RAG, cutting at paragraph, sentence or word boundaries. 0 turns chunks off. Otherwise at least 200.

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

Characters repeated at the start of the next chunk. Capped at half the chunk size. Only used when chunk size is above 0.

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

Larger files are skipped with an error, not charged. Hard cap 100 MB. Several large files at once need more run memory; the Actor lowers concurrency to fit.

## `downloadTimeoutSecs` (type: `integer`):

Per file, including redirects.

## `concurrency` (type: `integer`):

How many PDFs are processed at once (1 to 5).

## `mode` (type: `string`):

Internal: "selfTest" runs the maintainer health check and saves no items.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ],
  "maxPages": 100,
  "includeText": true,
  "includeMarkdown": true,
  "includePages": false,
  "chunkSize": 0,
  "chunkOverlap": 100,
  "maxFileSizeMb": 50,
  "downloadTimeoutSecs": 60,
  "concurrency": 3,
  "mode": "run"
}
```

# Actor output Schema

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

One item per PDF with status, text, markdown, metadata and optional pages and chunks (JSON, CSV, Excel or XML).

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

Counts per status, PDFs charged, bytes read and why the run stopped.

# 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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/pdf-to-text").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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("ataism/pdf-to-text").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.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
  ]
}' |
apify call ataism/pdf-to-text --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ataism/pdf-to-text"
        }
    }
}
```

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/K5YbUT7AUjZttWIrr/builds/T6rufh6xknVf8V8KM/openapi.json
