# Invoice Extractor with Per-Field Agreement Check (`herakles-dev/invoice-extractor`) Actor

Turns invoice PDFs and scans into JSON. Every field is marked as agreed by two independent readers or flagged for a person, and you pay only for pages it actually extracted and cross-checked.

- **URL**: https://apify.com/herakles-dev/invoice-extractor.md
- **Developed by:** [D. Michael Piscitelli](https://apify.com/herakles-dev) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$120.00 / 1,000 invoice page 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?

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

## Invoice Extractor with Per-Field Agreement Check

Turns invoice PDFs and scans into JSON. Every field is marked as agreed by two independent readers or flagged for a person, and you pay only for pages it actually extracted and cross-checked.

### What it does

I built this for one job: getting invoice data into your system without someone re-checking every number. You give it an invoice as a PDF or an image. Two separate readers go through it. One looks at the page image. The other works from the PDF's text, or from OCR text when the file is a scan. A field is marked `agree` only when both land on the same value. Anything else is flagged, and the Actor does not guess.

Fields: vendor name, vendor address, vendor tax ID, customer name, invoice number, invoice date, due date, PO number, currency, subtotal, tax, shipping, total, and line items with description, quantity, unit price and amount. Each field carries the page and the text snippet it came from, so a reviewer can check it against the source.

### Status of each field

- `agree`: both readers found the same value. Safe to accept automatically.
- `disagree`: they found different values. Send it to review.
- `single_source`: only one reader produced a value. Send it to review.
- `absent`: neither reader found the field on the page.
- `needs_locale`: the value is ambiguous, like the date 04/03/2026 with no locale given. Pass `localeHint` or send it to review.
- `unparsed`: a value was read but could not be turned into a clean date or number. Send it to review.

Each result begins with two counts, `verified_fields` and `needs_review`. A workflow can take the first group as is and route only the second to a person.

### Input example

```json
{
  "documents": ["https://example.com/invoices/INV-17744.pdf"],
  "localeHint": "en-US",
  "maxPages": 10
}
```

- `documents`: public http(s) links, up to 100 per run. Each file must be under 20 MB. PDF, PNG, JPEG, WebP and TIFF work.
- `recordKeys` and `recordStore`: for private files, keep the invoices as records in a key-value store in your Apify account and list their keys. Choose the store with the picker. Picking it is what gives the Actor read access, and it can read only the stores you pick. Leave `recordStore` empty to use the run's own store.
- `localeHint`: a locale tag such as `en-US`, `en-GB` or `de-DE`. It settles ambiguous dates and amounts like 1.234,56. Without it, an ambiguous value comes back as `needs_locale`.
- `maxPages`: 1 to 10, default 10. Pages past the limit are not read and not billed.

A bad link or a missing store gives an error on that document only. The rest of the run carries on.

### Output example (one dataset item per document, trimmed)

```json
{
  "source": "https://example.com/invoices/INV-17744.pdf",
  "outcome": "ok",
  "pages_total": 1,
  "pages_processed": 1,
  "pages_charged": 1,
  "verified_fields": 21,
  "needs_review": 0,
  "absent_fields": 0,
  "needs_review_fields": [],
  "notices": [],
  "extraction": {
    "data": {
      "vendor_name": "Harborview Supply Co.",
      "invoice_number": "INV-17744",
      "invoice_date": "2026-08-13",
      "due_date": "2026-08-27",
      "currency": "USD",
      "subtotal": 6378.99,
      "tax": 526.27,
      "total": 6905.26,
      "line_items": ["..."]
    },
    "fields": {
      "/total": { "status": "agree", "basis": "exact", "value": 6905.26, "evidence": { "page": 1, "snippet": "..." } }
    },
    "summary": { "agree": 21, "disagree": 0, "single_source": 0, "required_all_agree": true, "attention": [] }
  },
  "latency_s": 21.4
}
```

`outcome` is one of `ok`, `ok_degraded`, `not_an_invoice`, `skipped` or `error`. With `ok_degraded`, one reader was down, so the result is delivered but every value is single-source. A `skipped` document hit a limit (see `notices`). For `error`, `error.type` gives the reason.

### Pricing

$0.12 per page, pay per event (event name `page`). You are charged only for pages that were extracted and cross-checked, meaning an `ok` result. You are not charged when a reader is down, when the document is not an invoice, when a document fails, or when it is skipped. If you set a maximum total charge for the run, a document that would go past it is skipped and marked as such.

### Limits

- Free Apify plan: at most 3 pages per run. A page that fails to read still counts toward those 3. Paid plans: up to 10 pages per document.
- Up to 100 documents per run.
- Invoices only. Forms and receipts are not supported. They did not pass my accuracy testing, so I left them out. Phone photos of receipts may come back wrong or flagged.
- One-page invoices had a 95th-percentile time of 22.5 seconds in my test. The longest invoice in the set, dense with line items, took about 3.5 minutes.

### How I measured accuracy

I tested on 27 invoices from a held-out set, kept apart from the ones I tuned on. That is 38 pages, a mix of born-digital PDFs and scans. Every value that both readers agreed on was correct: 878 out of 878. On born-digital invoices the readers agreed on 89.6% of fields, and on scans 60.7%. The rest came back with another status so a person can look. This is my test set, not a promise for every layout. Keep the review step for anything that is not `agree`.

### Contact

Questions or a layout that misbehaves: hello@herakles.dev. I read it myself.

# Actor input Schema

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

Public http(s) links to invoice files. Up to 100 documents per run. Redirects are followed; each file must be under 20 MB.

## `recordKeys` (type: `array`):

Keys of invoice files you stored as records in a key-value store in your Apify account. Use this instead of URLs for private files.

## `recordStore` (type: `string`):

The key-value store that holds the records above. Pick it from the list: picking a store is what gives this Actor read access to it. Leave empty to use this run's own default store.

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

A locale tag such as en-US, en-GB or de-DE. It settles ambiguous dates (04/03/2026) and amounts (1.234,56). Without it, an ambiguous value comes back as needs\_locale instead of a guess.

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

Pages beyond this are not read and not billed. The limit is 10.

## Actor input object example

```json
{
  "documents": [],
  "recordKeys": [],
  "maxPages": 10
}
```

# Actor output Schema

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

One dataset item per document: outcome, pages processed and charged, the extracted invoice fields, and each field's status (agree, disagree, single\_source, absent, needs\_locale, unparsed).

## `overview` (type: `string`):

The same items in the dataset's overview view: one row per document with its outcome and counts.

# 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": [],
    "recordKeys": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("herakles-dev/invoice-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": [],
    "recordKeys": [],
}

# Run the Actor and wait for it to finish
run = client.actor("herakles-dev/invoice-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": [],
  "recordKeys": []
}' |
apify call herakles-dev/invoice-extractor --silent --output-dataset

```

## MCP server setup

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