# PDF to Text, Tables & Markdown (OCR) (`tidyrun/pdf-to-text`) Actor

Pull text, tables and Markdown out of PDFs in bulk, with OCR for scanned pages and invoices. One row per page, tables come back as rows you can use straight away, and failed PDFs are never charged.

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

## Pricing

from $0.40 / 1,000 pages

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 to Text, Tables & Markdown (OCR)

Turn **PDF links into clean text, Markdown and real table cells**, with **OCR for scanned pages** (English, Thai, Spanish, French, German and more). Each PDF becomes one row with its title, author and dates, a page-1 preview image, the full text, LLM-ready Markdown and every ruled table as rows and columns.

**$0.40 per 1,000 pages, $8 per 1,000 OCR pages, $0.0001 per run. Failed PDFs are free.**

### Quick start

1. Paste your **PDF URLs** (direct links that download the `.pdf` file).
2. Leave **OCR** on **Auto**: pages that already contain text are read directly, and only scanned pages go through OCR.
3. Scanned pages in Thai or another language? Pick it in **OCR languages**.
4. Click **Start**. The **Output** tab shows a preview of page 1, page and table counts and the first lines of text for every PDF.

The prefilled example runs a current IRS form (text and tables) and a scanned 1945 tax form (OCR), so your first run shows both.

Input example:

```json
{
    "urls": [{ "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf" }, { "url": "https://www.irs.gov/pub/irs-prior/f1040--1945.pdf" }],
    "ocr": "auto",
    "ocrLanguages": ["eng"],
    "pageRange": "1-10"
}
```

One output row per PDF (text and markdown shortened here):

```json
{
    "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf",
    "fileName": "fw4.pdf",
    "fileSizeBytes": 208845,
    "title": "2026 Form W-4",
    "pageCount": 5,
    "pagesProcessed": 5,
    "ocrPages": 0,
    "tableCount": 5,
    "previewImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/fw4-bac2c79c3f-page1.png",
    "metadata": { "title": "2026 Form W-4", "author": "C:DC:TS:CAR:MP", "subject": "Employee's Withholding Certificate", "creator": "Designer 6.5", "producer": "Designer 6.5", "created": "2025-12-11T07:15:41-06:00", "modified": "2025-12-11T07:15:41-06:00" },
    "snippet": "W-4 Employee’s Withholding Certificate OMB No. 1545-0074 Form Complete Form W-4 so that ...",
    "text": "W-4 Employee’s Withholding Certificate\nOMB No. 1545-0074\n...",
    "markdown": "<!-- page 1 -->\n\n## W-4 Employee’s Withholding Certificate\n\n...\n\n| $0 - 9,999 | $10,000 - 19,999 | ... |\n|---|---|---|",
    "tables": [{ "page": 5, "rows": [["$0 - 9,999", "$10,000 - 19,999", "..."], ["$0", "$280", "..."]] }],
    "textTruncated": false,
    "markdownTruncated": false,
    "truncatedAtMaxPages": false,
    "error": null
}
```

### Why this one

| Common problem | What happens here |
| --- | --- |
| Scanned PDFs come back empty | **Auto OCR** reads scanned pages with Tesseract. Pages with a text layer are never OCR'd, so you only pay OCR prices where OCR was needed. |
| Thai PDFs come out as `@ล!A ล01C` symbol soup | Pages whose text layer is unreadable (broken font maps) are detected and OCR'd instead. Thai OCR keeps words intact and fixes the split ำ vowel. |
| Tables flattened into lines of text | Ruled tables come back as `rows` of cells and as Markdown tables, placed where they appear on the page. |
| Two- and three-column layouts read across columns | Columns are detected and read top to bottom, column by column. |
| Hard to check results | The Output tab shows a page-1 thumbnail next to page, OCR and table counts. |
| Paying for broken links | Downloads that fail, web pages instead of PDFs, password-protected and damaged files are **never charged**, and the error says what to fix. |

### Pricing

| Event | Price |
| --- | --- |
| Page read from the PDF's text layer | $0.0004 |
| Page read with OCR (instead of the page price) | $0.008 |
| Run start | $0.0001 per run |

So a run with a 10-page report costs $0.0001 + $0.004, and a 4-page scanned form costs $0.0001 + $0.032. Put many PDFs in one run to pay the start only once. Blank pages count as normal pages. Pages outside **Pages** or past **Max pages per PDF** are skipped and free. The run stops cleanly at your maximum charge limit.

### Options

- **OCR**: *Auto* (recommended), *Always* (also OCR pages that have text, e.g. text hidden under a scan that is wrong) or *Off*.
- **Pages**: `1-5`, `1, 3, 10-` and so on. **Max pages per PDF** (200 by default) is a safety cap; rows cut by it have `truncatedAtMaxPages: true`.
- **PDF URLs from a dataset**: chain after a scraper by giving its dataset ID and the field that holds the PDF link.
- Very long documents: `text` and `markdown` hold up to 200,000 characters each. Longer ones are cut, flagged with `textTruncated` / `markdownTruncated`, and the full versions are linked in `textUrl` / `markdownUrl`.

### Use it from n8n

Install the Apify community node `@apify/n8n-nodes-apify` (it is already built into n8n Cloud), add an **Apify** node and point it at `tidyrun/pdf-to-text`. Two flows people build with it:

**Invoice tables into Google Sheets, every hour**

`Schedule` → `Read a queue tab` → `Keep rows with no status` → `Apify: tidyrun/pdf-to-text` → `Flatten tables into rows` → `Append to a Tables tab` → `Mark each PDF done`

One run handles every new PDF at once, and `extractTables` gives you the rows ready to append.

**Ask questions about your PDFs**

`Form upload` → `Apify: tidyrun/pdf-to-text` → `Split into chunks` → `Embed into a vector store` → `AI agent answers from it`

Markdown output keeps headings and tables, which is what makes the chunks worth retrieving.

### Call it from your code

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

### Good to know

- Tables are found from their grid lines. Tables drawn without lines (only spacing, or only horizontal rules as in many scientific papers) stay in `text` and `markdown` as lines, not as cells.
- OCR pages give text and Markdown paragraphs, not table cells.
- OCR quality depends on the scan. Clean 300 dpi scans read very well; faint typewriter copies and handwriting do not.
- Files up to 50 MB. Downloads time out after 2 minutes.
- Speed at the default 2 GB memory: about 0.3–1.5 s per text page and 6–20 s per OCR page. Each extra OCR language roughly doubles OCR time.
- PDFs whose own text layer is wrong but readable (for example a bad OCR layer added by a scanner) are kept as they are in Auto mode; use OCR *Always* for those.

### Responsible use

Only process documents you have the right to use. The Actor downloads public PDFs like a browser would; it does not log in, bypass paywalls or solve CAPTCHAs, and links that need a login are reported as errors and not charged. Files are deleted when each PDF is done; the only copies kept are the results in your own run's dataset and key-value store.

# Actor input Schema

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

Direct links to public PDF files (the link should download the .pdf itself, not a page that shows it).

## `ocr` (type: `string`):

Auto reads the PDF's own text where it exists and runs OCR only on scanned pages or pages whose text is unreadable. OCR pages are priced separately.

## `ocrLanguages` (type: `array`):

Languages printed in your scanned pages. Pick only the ones you need: each extra language makes OCR slower.

## `extractTables` (type: `boolean`):

Return tables with ruled grid lines as rows and cells (tables), and as Markdown tables in markdown.

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

Markdown with headings, paragraphs in reading order (two-column layouts included) and tables. Handy for LLMs and RAG.

## `includePageImages` (type: `boolean`):

Save a small PNG of the first processed page, shown in the Output tab.

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

Optional, e.g. 1-5 or 1, 3, 10-. Leave empty for all pages.

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

Safety cap. Pages after this are skipped (not charged) and the row is flagged truncatedAtMaxPages.

## `datasetId` (type: `string`):

Optional ID of a dataset (for example from a scraper run) that holds PDF links. Used together with the URLs above.

## `datasetField` (type: `string`):

Field in each dataset item that holds the PDF link (a URL or a list of URLs).

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-prior/f1040--1945.pdf"
    }
  ],
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "extractTables": true,
  "includeMarkdown": true,
  "includePageImages": true,
  "maxPages": 200,
  "datasetField": "url"
}
```

# Actor output Schema

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

One row per PDF: preview, page counts, OCR pages, tables, text snippet and any error.

## `fullResults` (type: `string`):

Every field of every row, including the full text, markdown and table cells.

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

Counts of PDFs, text pages, OCR pages, tables and failures.

## `files` (type: `string`):

Preview images, plus full text and Markdown files for very long PDFs.

# 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": [
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
        },
        {
            "url": "https://www.irs.gov/pub/irs-prior/f1040--1945.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidyrun/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": [
        { "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf" },
        { "url": "https://www.irs.gov/pub/irs-prior/f1040--1945.pdf" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidyrun/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": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw4.pdf"
    },
    {
      "url": "https://www.irs.gov/pub/irs-prior/f1040--1945.pdf"
    }
  ]
}' |
apify call tidyrun/pdf-to-text --silent --output-dataset

```

## MCP server setup

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