# PDF Text Extractor - PDF to Text, Markdown & Tables, by Page (`afterstar/pdf-to-text`) Actor

Extracts the text and tables of PDF files (by link), page by page. No OCR: scanned PDFs come back empty.

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

## Pricing

from $0.70 / 1,000 pdf pages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

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

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

## How to integrate an Actor?

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

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

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

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

# README

**Bulk PDF to text: paste links to your PDFs, press Start, and get every page's text, its tables and a Markdown version, ready to download as CSV, Excel or JSON. $1.00 per 1,000 PDF pages.**

### What you get back

One row per PDF page, in page order. Below is page 1 of California's public WARN report (16 pages; a sample file of the pdfplumber project), from our run on 13 October 2026, as returned. That run read 17 pages from two PDFs in 11.8 seconds.

`text` (first lines):

```
WARN Report*
Summary by Received Date
07/01/2015 - 03/25/2016
...
Notice Date Effective Received Company City No. Of Layoff/Closure
06/22/2015 03/25/2016 07/01/2015 Maxim Integrated Product San Jose 150 Closure Permanent
06/30/2015 08/29/2015 07/01/2015 McGraw-Hill Education Monterey 137 Layoff Unknown at this time
```

`tables` (each table is a list of rows, each row a list of cells; first rows):

```json
[["Notice Date", "Effective", "Received", "Company", "City", "No. Of", "Layoff/Closure"],
 ["06/22/2015", "03/25/2016", "07/01/2015", "Maxim Integrated Product", "San Jose", "150", "Closure Permanent"],
 ["06/30/2015", "08/29/2015", "07/01/2015", "McGraw-Hill Education", "Monterey", "137", "Layoff Unknown at this time"]]
```

`markdown` (the page text, then the same table as a Markdown table; the table part, first rows):

```
| Notice Date | Effective | Received | Company | City | No. Of | Layoff/Closure |
|---|---|---|---|---|---|---|
| 06/22/2015 | 03/25/2016 | 07/01/2015 | Maxim Integrated Product | San Jose | 150 | Closure Permanent |
```

Plus `page` (1), `pageCount` (16), `wordCount` (453) and `documentTitle` (empty for this file).

### How to extract text from many PDFs at once (no code)

1. Open the tool on Apify and sign in (a free Apify account; Google sign-in works).
2. In **PDF URLs**, paste your links, one per line (up to 50 MB and 500 pages per PDF by default).
3. Press **Start**.
4. In the run's **Output**, click **Export** and choose **CSV**, **Excel** or **JSON**. You get one row per page.

The links must be PDF files anyone can download (not a Google Drive or Dropbox *preview* page).

### PDF to Markdown, for ChatGPT, Claude or a RAG pipeline

Every page also comes as `markdown`: the page text, followed by each table on the page as a Markdown table, so a language model also sees the table structure (table cells appear twice: in the text and in the table). Join the pages in `page` order to get the whole document.

### PDF tables to Excel or CSV

Each table on a page comes in `tables` as rows of cells, and as a Markdown table inside `markdown`. In a JSON export the rows are ready to use; in a CSV or Excel export Apify spreads nested lists over many columns, so for tables JSON is the easier format. Not every table is detected: tables drawn without ruling lines (common in papers and in some reports) come as plain text lines in `text`, in reading order.

### Scanned PDFs

This tool reads the text that's already in the PDF and has no OCR, so scanned pages come back without text. For scans, use our [OCR Image to Text](https://apify.com/afterstar/image-ocr): it reads scanned PDFs page by page in 31 languages.

### Use it as a PDF to text API, or from n8n, Make and Zapier

Every run can also be started over Apify's API. This call reads one PDF and returns the rows as JSON when it's done (put your own Apify API token in place of `YOUR_TOKEN`):

```bash
curl -X POST "https://api.apify.com/v2/acts/afterstar~pdf-to-text/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/report.pdf"]}'
```

Add `&format=csv` to the address to get CSV instead of JSON. A synchronous call waits at most 5 minutes; for big batches, start a run (`/runs` instead of `/run-sync-get-dataset-items`) and read its dataset when it finishes. Apify's n8n, Make and Zapier integrations run the tool the same way.

### What does PDF Text Extractor do?

For each PDF link: one row per page with the page text, the tables as rows, the page text plus its tables as Markdown tables (`markdown`), the word count, the page count and the document title.

Run it once, or **schedule it to list only the pages that changed in your PDFs**. The tool remembers what it has seen, in a storage in your own Apify
account.

### What you get

|  |  |
|---|---|
| **Input** | Links to PDF files, one per line (up to 50 MB each, up to 500 pages by default) |
| **Per page** | The page text, its tables as rows, the page text plus its tables as Markdown tables (`markdown`), word count, page count, document title |
| **Schedule** | Only the pages that changed since the last run |
| **Price** | $1.00 per 1,000 PDF pages; on a schedule $1.20 per 1,000 unchanged documents checked |

### Use cases

- Turn reports, price lists or regulations into text for AI tools or search.
- Pull tables out of PDF reports into a spreadsheet.
- Watch a set of PDFs and get only the pages that changed.

### Input

| Field | What it does |
|---|---|
| PDF URLs | Links to PDF files, one per line |
| Max pages per PDF | Default 500 |
| Output mode | All results, only new results, or new + changed + removed results |

### Output

One row per PDF page (page 1 of the WARN report above, as a first run returns it; `text`, `tables` and `markdown` shortened with …, other keys left out):

```json
{
  "id": "pdf:https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf#p1",
  "changeType": "new",
  "url": "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf",
  "page": 1,
  "pageCount": 16,
  "documentTitle": null,
  "text": "WARN Report*\nSummary by Received Date\n07/01/2015 - 03/25/2016\n…",
  "tables": [[["Notice Date", "Effective", "Received", "Company", "City", "No. Of", "Layoff/Closure"], ["06/22/2015", "03/25/2016", "07/01/2015", "Maxim Integrated Product", "San Jose", "150", "Closure Permanent"], …]],
  "markdown": "WARN Report*\n…\n| Notice Date | Effective | Received | Company | City | No. Of | Layoff/Closure |\n…",
  "wordCount": 453
}
```

Every row has every key (`null` when there's no value). `changeType` is `new`, `changed`, `unchanged` or
`removed`; a `changed` row lists `changedFields` and the `previous` values. If a source fails or rate-limits the
request, you get one row with `changeType: "status"` and a `statusCode` instead of an error, and it's free.

### Schedule it

1. Fill in the input and **save it as a task**.
2. Run it once with **Baseline only** switched on: it remembers the current results and lists nothing. Or skip this
   and accept that the first scheduled run lists everything once.
3. Set **Output mode** to **Only new results** or **New, changed and removed**, and add a **schedule**.
4. Connect an **integration** (Slack, Google Sheets, Zapier, Make, n8n), or put a **webhook URL** in the input.

Each saved task keeps its own memory; switching a task between modes keeps it. Each run saves a summary (`OUTPUT` in
the run's key-value store) with the counts.

**Apify webhook template** (Integrations → Webhook → payload):

```json
{"datasetUrl": "https://api.apify.com/v2/datasets/{{resource.defaultDatasetId}}/items?clean=true&format=json"}
```

### How much does it cost?

- **$1.00 per 1,000 PDF pages** listed (on paid Apify plans 10-30% less).
- You pay only this price, no separate Apify platform usage. Apify's free plan includes $5 of credit a month, enough for about 5,000 PDF pages here.
- **On a schedule** (only new, changed or removed PDF pages): **$1.20 per 1,000 unchanged documents checked** plus $1.00 per 1,000 new, changed or removed PDF pages.
- **Example:** 1,000 PDF pages cost $1.00; watching 10 PDFs every day, where none changes, costs about $0.36 a month (plus Apify's start fee: $0.00005 per run).
- Status rows (errors) are free. Set a spending limit on the run and the tool stops there.

### Good to know

- No OCR: pages of scanned PDFs have no text. A PDF with no text at all gives a free status row.
- On a schedule, new, changed or removed pages cost $1 per 1,000 and an unchanged document $1.20 per 1,000.
- Files up to 50 MB. The tool follows each site's robots.txt.

### How it compares

| Instead of | With this tool |
|---|---|
| Copying text out of a PDF viewer | Every page of every PDF in one table, with tables kept as rows |
| Converting files one by one | A whole list of PDF links in one run, page by page |
| Re-reading reports that change | 'Changes' mode lists only the pages whose text changed |

### FAQ

**Does it read scanned PDFs?** No: it has no OCR, so pages of scanned PDFs come back without text, and a PDF with no text at all gives a free status row. For scans, use our [OCR Image to Text](https://apify.com/afterstar/image-ocr).

**How are tables returned?** Each table comes as rows (a list of cells) and as a Markdown table, next to the page text. Tables drawn without lines (common in papers) and chart frames are not returned as tables; the page text keeps every word in reading order.

**Can I upload a PDF?** Give a link to the PDF file. The tool follows each site's robots.txt.

**How big can a PDF be?** Up to 50 MB, and up to 500 pages by default (Max pages per PDF).

**What does a schedule cost?** New, changed or removed pages cost $1 per 1,000 and an unchanged document $1.20 per 1,000.

**Source:** the PDF files you link to. Extraction by pdfplumber (MIT).

### More Afterstar tools

- [Audio Transcription & Speech to Text - Video, Podcasts, SRT](https://apify.com/afterstar/audio-transcriber): Turns audio and video files (your own uploads, direct file links, public podcast feeds) into text and subtitles (SRT/VTT) in ~99 languages, on the open-source Whisper model, run inside the tool (no API key).

Found a problem or need a field? Open an issue on the Issues tab.

# Actor input Schema

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

Links to PDF files, one per line.

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

The most pages read from each PDF.

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

'All' lists everything found now. 'Only new' lists results never seen in earlier runs. 'New, changed and removed' also lists results that changed or disappeared. The first scheduled run counts everything as new; use 'Baseline only' to start cheaply.

## `baselineOnly` (type: `boolean`):

Saves what exists now without listing it, so the next scheduled run shows only what's new. Charged like a run with nothing new.

## `stateKey` (type: `string`):

Leave empty: each saved task keeps its own memory. Set a name to share one memory between tasks, or a new name to start over.

## `resetState` (type: `boolean`):

Starts from an empty memory: every result counts as new again.

## `webhookUrl` (type: `string`):

We POST a summary plus up to 100 new, changed or removed results here when something changed. Works with Slack incoming webhooks (choose the Slack format), Zapier, Make and n8n.

## `webhookFormat` (type: `string`):

JSON for automation tools; 'Slack message' sends one line of text.

## `webhookAlways` (type: `boolean`):

By default the webhook is called only when something changed or a source failed.

## Actor input object example

```json
{
  "urls": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
  ],
  "maxPagesPerPdf": 500,
  "outputMode": "all",
  "baselineOnly": false,
  "stateKey": "",
  "resetState": false,
  "webhookUrl": "",
  "webhookFormat": "json",
  "webhookAlways": false
}
```

# 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 = {
    "urls": [
        "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("afterstar/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 = { "urls": ["https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("afterstar/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 '{
  "urls": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
  ]
}' |
apify call afterstar/pdf-to-text --silent --output-dataset

```

## MCP server setup

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