# Pdf Invoice Table Extraction (`first_watch/pdf-invoice-table-extraction`) Actor

Automation workflows need to convert varied native-text or scanned invoices into consistent accounting-ready line items while detecting uncertain fields and arithmetic mismatches before downstream import.

- **URL**: https://apify.com/first_watch/pdf-invoice-table-extraction.md
- **Developed by:** [Jordan Nabbe](https://apify.com/first_watch) (community)
- **Categories:** Automation, Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $30.00 / 1,000 result returneds

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 Invoice Table Extraction

PDF Invoice Table Extraction turns invoices into clean, accounting-ready JSON and CSV for automation builders, accounts-payable teams and agencies who connect invoice intake to ERP, accounting or workflow systems. It reads real native-text PDF files (base64 or HTTPS URL) plus text, HTML, CSV and JSON invoices, normalizes header fields and line items, recomputes the totals and tells you exactly which invoices need a human look before import.

### What it does

- Parses PDF bytes with a real PDF parser, page by page, and keeps the source page of every line item.
- Also accepts inline text, HTML tables, CSV line-item tables, JSON invoice records, and base64-encoded versions of these.
- Extracts vendor, tax id, invoice number, invoice and due date, currency, PO number, subtotal, discount, tax, shipping, grand total and line items (description, SKU, quantity, unit price, discount, tax, line total).
- Normalizes dates to ISO `YYYY-MM-DD` and amounts to plain numbers. European and US notation are both understood.
- Never guesses ambiguous values. `1.234` vs `1,234` and `03/04/2025` are rejected with `AMBIGUOUS_NUMBER` or `AMBIGUOUS_DATE` unless the document settles them (for example `13/04/2025` or `1.234,56`) or you set `localeHint`.
- Reconciles line totals, subtotal and grand total within a configurable tolerance. Stated values are never overwritten; stated and calculated values are both returned with the difference.
- Reports evidence lines with page numbers and explicit exceptions (`TOTAL_MISMATCH`, `SUBTOTAL_MISMATCH`, `LINE_TOTAL_MISMATCH`, `MISSING_FIELD`, `INVALID_VALUE`, ...). No confidence scores are invented; only measured checks are reported.
- Supports vendor column aliases and a `targetSchema` that renames canonical fields in an extra `mappedInvoice` copy.
- Writes one dataset row per extracted invoice, an `OUTPUT` record with the summary, all rows, per-source failures and an outcome block, and an `OUTPUT.csv` line-item file.
- One bad source never fails the batch; it is reported in `OUTPUT` with an error code. If nothing usable was produced, the run fails after writing `OUTPUT`.

### How to use it

Paste sources into the input and run. Example:

```json
{
  "sources": [
    {
      "mode": "inline",
      "name": "acme-invoice-1001.txt",
      "format": "text",
      "content": "ACME Supplies\nInvoice: INV-1001\nInvoice Date: 2025-01-15\nCurrency: USD\n\nDescription,SKU,Qty,Unit Price,Line Total\nBlue widgets,BW-10,2,10.00,20.00\n\nSubtotal: 20.00\nTax: 2.00\nTotal: 22.00"
    },
    { "mode": "url", "name": "vendor-b.pdf", "url": "https://files.example.com/invoices/b.pdf", "format": "pdf" }
  ],
  "currencyHint": "USD",
  "localeHint": "en-US",
  "reconciliationTolerance": 0.02
}
```

For a PDF you already hold, use `{"mode":"inline","name":"a.pdf","format":"base64_document","mediaType":"application/pdf","content":"<base64>"}`.

Tables in text and PDFs are detected from a header row whose columns match known names (Description, SKU, Qty, Unit Price, Line Total and aliases), separated by commas, tabs, pipes or two or more spaces. Header fields are read from `Label: value` lines.

### Output

| Field | Meaning |
|---|---|
| inputName, source | Your source name and a query-free source reference |
| status | `succeeded` when invoice number, grand total and line items were extracted; `partial` when an invoice was produced but one of those is missing |
| reviewStatus | `clean`, or `needs_review` when any warning was raised (for example a total mismatch) |
| vendorName, invoiceNumber, currency, grandTotal, lineItemCount | Flat columns for spreadsheets |
| invoice | Canonical invoice with `lineItems` |
| reconciliation | Calculated vs stated subtotal and grand total, difference, withinTolerance |
| sourceReferences | Evidence line and page per field |
| exceptions | code, field, severity, message |

Abridged row:

```json
{ "inputName": "acme-invoice-1001.txt", "recordType": "invoice", "status": "succeeded", "reviewStatus": "clean", "invoiceNumber": "INV-1001", "grandTotal": 38.5,
  "reconciliation": { "calculatedGrandTotal": 38.5, "statedGrandTotal": 38.5, "difference": 0, "withinTolerance": true },
  "reviewRequired": false }
```

The `OUTPUT` record adds `outcome` with `status` (SUCCEEDED, PARTIAL, FAILED or LIMIT_REACHED), `itemsReceived`, `itemsSucceeded`, `itemsFailed`, `billableCount`, `limitReached` and `undelivered`. Sources that could not be read (for example `INVALID_BASE64`, `URL_BLOCKED`, `OCR_REQUIRED`) appear only in `OUTPUT.rows` with `status: failed`, never in the dataset.

### Pricing

Pay per event. The main charge is per invoice; everything else listed below is free.

| Event | Price (USD) | Charged when |
|---|---|---|
| Actor start (`apify-actor-start`) | $0.005 | The run starts. |
| Invoice processed (`apify-default-dataset-item`) | $0.03 | An extracted invoice row (status succeeded or partial) is written to the default dataset. Sources that could not be read are never written to the dataset. |

Not charged:

- failed source rows (OUTPUT only)
- OUTPUT summary
- OUTPUT.csv line-item file

Examples (default 512 MB memory, one start event):

- 10 invoices processed: $0.005 start + 1 × $0.005 + 10 × $0.03 = **$0.31**
- 8 invoices, 2 sources failed: $0.005 start + 1 × $0.005 + 8 × $0.03 = **$0.25**

Spending limit: when you set a maximum cost per run, the Actor stops before it would exceed it, keeps every invoice it already delivered and reports `limitReached` in the `OUTPUT` record. It never delivers results beyond the limit.

The Store's Pricing tab shows the prices in force. If this section and the Pricing tab ever differ, the Pricing tab applies.

### Use cases

- Normalize invoice headers and line items before ERP or accounting import.
- Catch subtotal, tax, shipping and grand-total mismatches automatically.
- Queue invoices with `needs_review` for human review.
- Map different vendors' column names onto one schema.
- Convert PDF, HTML, CSV or JSON invoices to JSON and CSV.

### Limits

- At most 50 sources per run, 1000 rows and 1000 line items per invoice; 10 MiB per inline source, 25 MiB per download; up to 500 PDF pages (`maxPagesPerDocument` defaults to 100).
- PDFs must contain a text layer. Scanned or image-only PDFs are reported with `OCR_REQUIRED`; there is no OCR. Encrypted or damaged PDFs give `PDF_TEXT_UNSUPPORTED`.
- Extraction is rule-based. Unusual layouts, or tables separated by single spaces, may be missed; those invoices surface as exceptions rather than silent wrong values.
- `minimumConfidence` is accepted for compatibility but has no effect, because no confidence scores are produced.
- URL mode: HTTPS only, public hosts only (private, loopback and link-local addresses and embedded credentials are rejected, redirects are re-validated, at most 3), 30-second timeout.
- Spreadsheet (.xlsx) input is not supported; export the sheet to CSV first.
- If your spending limit is reached, remaining invoices are not delivered and `OUTPUT.outcome` reports them as `undelivered` with `limitReached: true`.
- Invoice content is never written to the log. Datasets and key-value stores follow your Apify retention settings.

### FAQ

**Does it work offline?** Yes, for all inline sources, including base64 PDFs.

**Does it read scanned invoices?** No. It reports `OCR_REQUIRED` for that source.

**Are totals silently corrected?** Never. Stated values are kept, the difference is reported and the row is marked `needs_review`.

**What if the number format is ambiguous?** The value is left empty and an ambiguity exception is added, unless `localeHint` settles it.

**What happens with invalid input?** The run fails before processing with `INPUT_VALIDATION_ERROR`.

**Is my invoice data shared or used for training?** No. Extraction is rule-based and runs inside your Actor run. In URL mode only the exact HTTPS URL you supply (and its validated redirects) is downloaded.

# Actor input Schema

## `sources` (type: `array`):

1 to 50 sources. Inline: {mode:'inline', name, format: records|text|html|csv|json|base64_document, content, mediaType?}. URL: {mode:'url', name, url (https only), format?: auto|text|html|csv|json|pdf}.

## `currencyHint` (type: `string`):

ISO 4217 code used when the document does not state a currency, e.g. USD.

## `localeHint` (type: `string`):

Settles ambiguous notation such as 1.234 vs 1,234 or 03/04/2025. de-DE, fr-FR, es-ES etc. use comma decimals and day-first dates; en-US uses month-first dates. Without a hint, ambiguous values are rejected with AMBIGUOUS_NUMBER or AMBIGUOUS_DATE.

## `maxPagesPerDocument` (type: `integer`):

Pages beyond this limit are ignored and reported as a warning.

## `minimumConfidence` (type: `number`):

Accepted for compatibility. The Actor reports evidence lines, exceptions and reconciliation instead of confidence scores, so this value has no effect.

## `reconciliationTolerance` (type: `number`):

Maximum absolute difference between stated and calculated amounts.

## `vendorAliases` (type: `object`):

Map canonical fields to extra labels or column names, e.g. {'invoiceNumber': \['Bill No'], 'lineTotal': \['Ext Amount']}.

## `targetSchema` (type: `object`):

Optional canonical-to-customer field name mapping. Produces a mappedInvoice copy, e.g. {'invoiceNumber': 'bill_id'}.

## Actor input object example

```json
{
  "sources": [
    {
      "mode": "inline",
      "name": "acme-invoice-1001.txt",
      "format": "text",
      "mediaType": "text/plain",
      "content": "ACME Supplies\nInvoice: INV-1001\nInvoice Date: 2025-01-15\nDue Date: 2025-02-14\nCurrency: USD\n\nDescription,SKU,Qty,Unit Price,Line Total\nBlue widgets,BW-10,2,10.00,20.00\nPacking tape,PT-2,3,5.00,15.00\n\nSubtotal: 35.00\nTax: 3.50\nShipping: 0.00\nTotal: 38.50"
    }
  ],
  "currencyHint": "USD",
  "localeHint": "en-US",
  "maxPagesPerDocument": 100,
  "minimumConfidence": 0.75,
  "reconciliationTolerance": 0.02,
  "vendorAliases": {},
  "targetSchema": {}
}
```

# Actor output Schema

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

Every result row stored in the default dataset.

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

Summary written to the OUTPUT record.

# 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 = {
    "sources": [
        {
            "mode": "inline",
            "name": "acme-invoice-1001.txt",
            "format": "text",
            "mediaType": "text/plain",
            "content": "ACME Supplies\nInvoice: INV-1001\nInvoice Date: 2025-01-15\nDue Date: 2025-02-14\nCurrency: USD\n\nDescription,SKU,Qty,Unit Price,Line Total\nBlue widgets,BW-10,2,10.00,20.00\nPacking tape,PT-2,3,5.00,15.00\n\nSubtotal: 35.00\nTax: 3.50\nShipping: 0.00\nTotal: 38.50"
        }
    ],
    "currencyHint": "USD",
    "localeHint": "en-US",
    "vendorAliases": {},
    "targetSchema": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("first_watch/pdf-invoice-table-extraction").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 = {
    "sources": [{
            "mode": "inline",
            "name": "acme-invoice-1001.txt",
            "format": "text",
            "mediaType": "text/plain",
            "content": """ACME Supplies
Invoice: INV-1001
Invoice Date: 2025-01-15
Due Date: 2025-02-14
Currency: USD

Description,SKU,Qty,Unit Price,Line Total
Blue widgets,BW-10,2,10.00,20.00
Packing tape,PT-2,3,5.00,15.00

Subtotal: 35.00
Tax: 3.50
Shipping: 0.00
Total: 38.50""",
        }],
    "currencyHint": "USD",
    "localeHint": "en-US",
    "vendorAliases": {},
    "targetSchema": {},
}

# Run the Actor and wait for it to finish
run = client.actor("first_watch/pdf-invoice-table-extraction").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 '{
  "sources": [
    {
      "mode": "inline",
      "name": "acme-invoice-1001.txt",
      "format": "text",
      "mediaType": "text/plain",
      "content": "ACME Supplies\\nInvoice: INV-1001\\nInvoice Date: 2025-01-15\\nDue Date: 2025-02-14\\nCurrency: USD\\n\\nDescription,SKU,Qty,Unit Price,Line Total\\nBlue widgets,BW-10,2,10.00,20.00\\nPacking tape,PT-2,3,5.00,15.00\\n\\nSubtotal: 35.00\\nTax: 3.50\\nShipping: 0.00\\nTotal: 38.50"
    }
  ],
  "currencyHint": "USD",
  "localeHint": "en-US",
  "vendorAliases": {},
  "targetSchema": {}
}' |
apify call first_watch/pdf-invoice-table-extraction --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,first_watch/pdf-invoice-table-extraction"
        }
    }
}
```

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/l9QgfMkmSsBnem83C/builds/k23lcm6VFHZi5pGig/openapi.json
