# PDF Text and Table Extractor (`odinbrs/pdf-text-and-table-extractor`) Actor

Extract native PDF text and grid tables into JSON, text and CSV. HTTPS/base64 input, duplicate detection and clear partial/OCR statuses. Pay only for PDFs completed without technical warnings.

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

## Pricing

$10.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?

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

### What does PDF Text and Table Extractor do?

Extract **selectable PDF text and tables with visible borders** into page-level JSON, plain text, and a CSV of table cells. Send public HTTPS PDF links or base64-encoded files. Run the built-in synthetic example without preparing an input file.

This Actor runs on Apify, so it can be invoked through the platform API and connected to existing workflows. It does not call an AI service, use a proxy, or perform OCR. A successful run means the program completed: inspect each document's status to identify partial results and files that need OCR.

### Why use this extractor?

Convert native PDF reports and simple product tables into reusable data. Every table keeps its page number, row, and column, making review easier. Decimal commas, leading zeroes, currency symbols, and negative numbers remain strings in JSON. Repeated files in the same batch are detected using SHA-256 and point to the first extracted result.

This is a deterministic extraction tool. It does not interpret invoices, infer missing cells, validate totals, or certify the accuracy of a document. Tables without borders, unusual fonts, merged cells, multi-column layouts, and images may need other processing.

#### Practical workflows

| Your input | What you can automate | What still needs review |
| --- | --- | --- |
| A supplier price list exported as a PDF with grid lines | Collect the detected table cells as JSON or CSV, then map the relevant columns in your workflow | Which column is the SKU, currency, unit, or price; these fields are not inferred |
| A native PDF report | Retrieve page-level text and tables for search, indexing, or downstream analysis | Reading order and complex layouts |
| A batch with repeated PDFs | Extract each unique file once within that run and keep duplicate references | Deduplication across separate runs belongs in your workflow |

Try the built-in two-page table demo first. It includes `002`, `1.234,56`, `-12,50`, and `USD 1,234.56`, so you can inspect how strings are preserved in JSON before connecting real documents.

### How to use

1. For a first test, leave Documents empty and enable Run built-in demo.
2. For your own authorized files, enter HTTPS URLs or objects with a source and optional name.
3. Choose a page limit, then start the Actor.
4. Inspect the document statuses in the dataset. Open Storage for each document's JSON, text, and CSV records.

### Input

```json
{
  "documents": [{"url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "name": "sample.pdf"}],
  "runDemo": false,
  "maxPages": 20,
  "extractTables": true
}
```

Use exactly one of `url` or `base64` per object. Maximum: 10 documents per run, 10 MB per PDF, 50 pages per document, and 45 seconds of worker time per document. Page limit defaults to 20. Each document's extracted JSON is limited to approximately 4 MB. The platform run is configured separately with a 600-second timeout and 512 MB memory. HTTPS port 443 is required; authenticated URLs with username/password and private-network destinations are rejected. Download redirects are limited to three and each destination is checked.

### Output

`OUTPUT` contains the run summary and all document summaries. Each unique parsed file has `document-001-json` and `document-001-text` records. With table extraction enabled, `document-001-tables` is a CSV in long cell format: page, table, row, column, value. The dataset can also be downloaded using Apify's JSON, CSV, and Excel exports; it contains summaries, not the full nested table data.

```json
{
  "doc_id": "document-001",
  "status": "ok",
  "pages_total": 2,
  "pages_processed": 2,
  "table_count": 2,
  "warnings": [],
  "json_key": "document-001-json"
}
```

### Automate with Python

Install `apify-client>=3.2,<4` and supply your own `APIFY_TOKEN` through your environment or secret manager. This example starts the built-in demo, waits, and retrieves the **full document JSON**, not just the dataset summary. Keep the printed run ID: use the recovery block below if a connection fails.

```python
import json
import os
from datetime import timedelta
from decimal import Decimal
from apify_client import ApifyClient

## Disable automatic retries for the start request to avoid an ambiguous restart.
client = ApifyClient(os.environ["APIFY_TOKEN"], max_retries=0)
run = client.actor("odinbrs/pdf-text-and-table-extractor").start(
    run_input={"documents": [], "runDemo": True,
               "maxPages": 20, "extractTables": True},
    max_total_charge_usd=Decimal("0.01"),
    run_timeout=timedelta(seconds=600),
    memory_mbytes=512,
    restart_on_error=False,
)
print("Keep this run ID:", run.id, flush=True)

## Recovery: replace run.id with your existing run ID to download without a new run.
finished = client.run(run.id).wait_for_finish(wait_duration=timedelta(seconds=660))
if finished is None or finished.status not in {"SUCCEEDED", "FAILED", "TIMED-OUT", "ABORTED"}:
    raise RuntimeError("Run still active or unavailable. Check the same run in Console.")
store = client.key_value_store(finished.default_key_value_store_id)
record = store.get_record("OUTPUT")
if record is None:
    raise RuntimeError("No OUTPUT available; inspect this run in Console.")
summary = record["value"]
print(json.dumps(summary, ensure_ascii=False, indent=2))
for document in summary["results"]:
    if document.get("json_key"):
        full_record = store.get_record(document["json_key"])
        if full_record is None:
            raise RuntimeError("Document record unavailable; check retention and this run.")
        print(json.dumps(full_record["value"], ensure_ascii=False, indent=2))
```

For your own PDFs, replace the input with the JSON example above and set `runDemo` to `false`. The maximum charge of $0.01 permits one billable document; increase it to $0.10 for up to ten. Check `requested_documents`, `documents`, `stop_reason`, each document's `status`, and `billing_status`. A `SUCCEEDED` run can contain partial output. A failed run may still have recoverable records.

To retrieve plain text or CSV, call `store.get_record_as_bytes(document["text_key"])` or `store.get_record_as_bytes(document["tables_key"])` when that key exists, then save the returned `value` bytes. A duplicate points to `duplicate_of`; it does not have a second full record. Download before storage retention expires. If the initial start request fails before returning an ID, inspect your Console Runs before starting again.

See the official [Python Actor client](https://docs.apify.com/api/client/python/reference/class/ActorClient) and [key-value store client](https://docs.apify.com/api/client/python/reference/class/KeyValueStoreClient) for connection options.

### Connect n8n or Make

In **n8n**, configure the official Apify node with your own credentials. Use **Run Actor**, select this Actor, pass the Input JSON, and enable **Wait for finish**. Use the returned run's `defaultKeyValueStoreId` in **Key-Value Stores → Get Record**, with key `OUTPUT`. Iterate its `results` and retrieve the `json_key`, `text_key`, or `tables_key` records you need. Filter or route documents using their status before updating another system. See the [Apify n8n setup guide](https://docs.apify.com/integrations/n8n).

In **Make**, use Apify's **Run an Actor** module and wait for completion. The ordinary **Get Dataset Items** operation returns summaries only. For the full content, use the run's `defaultKeyValueStoreId` to retrieve `OUTPUT` and then its document keys through an authenticated HTTP request:

```text
GET https://api.apify.com/v2/key-value-stores/{defaultKeyValueStoreId}/records/OUTPUT
GET https://api.apify.com/v2/key-value-stores/{defaultKeyValueStoreId}/records/{json_key}
Authorization: Bearer YOUR_APIFY_TOKEN
```

Store authentication in the integration's credential settings, never a shared URL. If a module does not expose the maximum charge option, the platform HTTP API accepts `maxTotalChargeUsd` when starting a run. Avoid automatic retries of the run-start step; recover the existing run instead. Set a 600-second Actor timeout and use polling or a completion trigger if your integration cannot wait that long. Integration steps are based on the official [Make guide](https://docs.apify.com/integrations/make); the exact modules available depend on your installation. No n8n or Make workspace is provisioned by this Actor.

### Data fields and statuses

| Field | Meaning |
| --- | --- |
| status | ok, partial, needs\_ocr, no\_extractable\_text, duplicate, or error |
| pages\_total / pages\_processed | File page count versus extracted pages |
| table\_count | Detected grid tables; zero does not prove no table exists |
| warnings | Page limit, output limit, missing text, or a complex table layout requiring review |
| duplicate\_of | Original document identifier for repeated bytes |
| error | Safe error code, without credentials or source URL |
| sha256 | Hash of original bytes, for audit and duplicate detection |

An `ok` status indicates that extraction completed without the listed technical warnings. It is not an accuracy score. A page containing both text and scanned content may still need manual review because this version does not recognize text inside images.

Tables containing spanning cells or multiline cells trigger a `complex_layout_review` warning and a `partial` document status. These structures can combine several apparent rows into one cell. The raw extraction remains available for inspection; this version does not guess how to split those cells.

### Pricing and cost estimation

**US$ 0.01 per unique PDF with status `ok`** ($10 per 1,000 completed documents). There is no start fee or additional platform-usage charge for users of this pay-per-event Actor. The live Pricing tab is authoritative.

The built-in demo, duplicates within a run, errors, blank files, OCR-only files, and all `partial` results have no extraction charge. Partial output is still available to inspect. A technically successful extraction is not a guarantee that every cell was recognized correctly.

Examples: five unique `ok` PDFs cost $0.05. Two `ok` PDFs plus one duplicate and one scan cost $0.02. The built-in demo costs $0. Set the run's maximum charge high enough for the number of documents you want processed ($0.10 for ten). The Actor stops before the next document if the remaining limit cannot pay for one event; unprocessed inputs are counted by comparing `requested_documents` with `documents` in `OUTPUT`.

Full result records, the dataset summary, and `OUTPUT` are saved before an extraction is charged. `OUTPUT` is updated with the final `billing_status` and `charged_events`; the dataset is a snapshot from before charging and can show `pending`. On an ambiguous billing response the run stops and reports `unconfirmed`, retaining the extracted output. It does not repeat the charge automatically.

Automatic replay of a previously started run (resurrection or migration) is disabled to avoid duplicate charges. Open that run's existing storage to recover saved results. Starting a separate new run is a new extraction and may charge again; deduplication applies within a run only.

### Tips

Use small batches and native PDFs with visible table lines. Lower the page limit for previews. JSON preserves original extracted strings. To reduce spreadsheet formula injection risk, the CSV adds an apostrophe before cells beginning with formula-like characters, including a minus sign; use JSON when exact strings matter. Source URLs are not copied into output summaries or logs, but the platform retains the submitted input. Do not put confidential URLs in a public example.

Results are stored in the run's Apify storage and follow the account's retention and access settings. Export results before retention expires. This tool does not promise archival storage. Use only documents you are authorized to process.

### FAQ and support

**Does this read scans or handwriting?** No. Image-only pages are marked `needs_ocr`; no paid fallback runs automatically.

**Does it preserve a spreadsheet's original formatting?** No. It returns detected cells, coordinates, and text. Check important values against the PDF.

**What happens on failure?** Each document gets a status and safe error code. If every document fails, the run fails after saving the summary, enabling platform failure alerts. Partial batches keep the successful results.

**How do I report a problem?** Use this Actor's Issues tab with the run ID, document status, and a small reproducible example that you are allowed to share. Do not post confidential PDFs, access tokens, or signed download links. Validate important values against the original PDF before relying on them.

# Actor input Schema

## `documents` (type: `array`):

Up to 10 public HTTPS URLs, or objects with url or base64 and optional name. Only documents you have permission to process. 10 MB maximum per PDF.

## `runDemo` (type: `boolean`):

Processes a synthetic two-page table PDF without an external download or extraction charge.

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

Stops at this page limit and reports partial results; never silently truncates.

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

Detect tables with visible ruling lines. Borderless tables and OCR are not supported.

## Actor input object example

```json
{
  "documents": [],
  "runDemo": true,
  "maxPages": 20,
  "extractTables": true
}
```

# Actor output Schema

## `documents` (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 = {
    "documents": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("odinbrs/pdf-text-and-table-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 = { "documents": [] }

# Run the Actor and wait for it to finish
run = client.actor("odinbrs/pdf-text-and-table-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 '{
  "documents": []
}' |
apify call odinbrs/pdf-text-and-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,odinbrs/pdf-text-and-table-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/vWntDJGBcnlePjiwK/builds/qo5eh9m6Df7M953tm/openapi.json
