# PDF Text and Tables Extractor (`timbered_oak/pdf-text-and-tables`) Actor

Extracts text and heuristically-detected tables from arbitrary PDF URLs, no site or proxy required.

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

## Pricing

$17.00 / 1,000 pdf processed (text + tables)s

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF Text and Tables Extractor

### What it does

Fetches each PDF in `pdfUrls`, extracts full text, per-page text, and
heuristically-detected tables — no site to crawl, no proxy needed. Works on
any publicly reachable PDF URL (government reports, technical papers,
filings, etc.).

### Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `pdfUrls` | array of string | yes | — | PDF URLs to fetch. |
| `maxPages` | integer | no | 200 | Stop extracting after this many pages per PDF. |
| `extractTables` | boolean | no | true | Detect tables (see Table detection below). |
| `pagesAsRows` | boolean | no | false | `false`: one dataset row per PDF (full text + `pages[]`). `true`: one row per page. |

### Table detection (heuristic — read this before relying on it)

Tables are **not** parsed from real table structure (no ruled-line or
cell-border detection). A "table" is: text items on the same page grouped by
rounded y-coordinate into lines, each line split into cells wherever the
horizontal gap between adjacent text items exceeds ~10pt, kept only if a line
has 3+ such gaps. This catches simple grid-aligned tables (the kind common
in government/technical PDFs) and will miss or mis-split anything with
merged cells, multi-line cell text, or unusual column spacing. Treat
`tables[]` as "candidate tabular rows to review," not ground truth.

### Output example

One row per PDF (default, `pagesAsRows: false`):

```json
{
  "url": "https://www.ntsb.gov/investigations/AccidentReports/Reports/AIR2401.pdf",
  "title": "American Airlines Flight 106, Boeing 777-200, N754AN, ...",
  "pageCount": 78,
  "text": "Runway Incursion and Rejected Takeoff American Airlines Flight 106 ...",
  "pages": [{ "page": 1, "text": "..." }, { "page": 2, "text": "..." }],
  "tables": [{ "page": 41, "rows": [["Date", "NTSB case number", "Location", "Event description"], ["April 17, 2024", "DCA24FA164", "Queens, New York", "Runway incursion"]] }],
  "byteSize": 2469207,
  "fetchedAt": "2026-09-01T12:00:00.000Z"
}
```

With `pagesAsRows: true`, the same fields are emitted once per page (`page`

- that page's `text`/`tables` instead of the full arrays).

A non-PDF URL (wrong content-type / not a `%PDF-` file) produces an
uncharged error row: `{ "url": "...", "error": "Not a PDF (content-type: ...)" }`.
A fetch or parse failure produces `{ "url": "...", "error": "<message>" }`,
also uncharged.

### Pricing

Pay-per-event (PPE). One `pdf-processed` event is charged **per PDF fetched
and parsed** — once, regardless of `pagesAsRows` or page count. Price and
description are set in the Apify Console's Actor pricing step, not in this
repo: **$0.017 per `pdf-processed` event ($17/1,000 PDFs)**, 15% under the
incumbent's $20/1,000 with no per-page or start fee (`docs/CANDIDATES.md` #13,
`policy/RULES.md` rule 3). Never charged for a rejected non-PDF or a fetch/parse
error.

### Limits

- Scanned/image-only PDFs are not OCR'd — text extraction returns little or
  no text for them (this Actor does not detect and refuse them separately;
  check `text.length` in your own pipeline).
- Free-plan compute only (`policy/RULES.md` rule 4); very large PDFs (many
  hundreds of pages) may hit the memory ceiling — use `maxPages` to cap.
- No residential proxy, no personal data in output (rule 1).
- Table extraction is heuristic — see the section above.

# Actor input Schema

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

URLs of PDF documents to fetch and extract.

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

Stop extracting after this many pages (protects memory on huge files).

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

Heuristically detect and extract tables (lines sharing a y-coordinate with 3+ column gaps). Off = text only.

## `pagesAsRows` (type: `boolean`):

false (default): one dataset row per PDF with full text + a pages array. true: one dataset row per page.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.ntsb.gov/investigations/AccidentReports/Reports/AIR2401.pdf"
  ],
  "maxPages": 200,
  "extractTables": true,
  "pagesAsRows": false
}
```

# Actor output Schema

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

All scraped rows as JSON

# 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.ntsb.gov/investigations/AccidentReports/Reports/AIR2401.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("timbered_oak/pdf-text-and-tables").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.ntsb.gov/investigations/AccidentReports/Reports/AIR2401.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("timbered_oak/pdf-text-and-tables").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.ntsb.gov/investigations/AccidentReports/Reports/AIR2401.pdf"
  ]
}' |
apify call timbered_oak/pdf-text-and-tables --silent --output-dataset

```

## MCP server setup

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

```

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/E6YJ1lSfdIHPIp3wp/builds/dqxT488QK1X2Dy6fa/openapi.json
