# Invoice Batch Validator: PDF Invoices to JSON+CSV (`seasons-ai/invoice-batch-validator`) Actor

Extract invoice PDFs, scans and photos to line-item JSON and CSV, re-check line math, tax and totals, and flag invoices that need review. Pay per invoice.

- **URL**: https://apify.com/seasons-ai/invoice-batch-validator.md
- **Developed by:** [Stan Lookoff](https://apify.com/seasons-ai) (community)
- **Categories:** AI, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$50.00 / 1,000 invoice processeds

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 Batch Validator: PDF & Image Invoices to Checked JSON + CSV

Turn a batch of supplier invoices (PDFs, scans or photos) into structured JSON and CSV, and let the Actor **recompute the arithmetic** so you only look at the invoices that don't add up.

### What you can do

- **Extract every invoice field:** vendor, customer, invoice number, dates, currency, every line item, subtotal, discounts, shipping, tax and total.
- **Catch math errors automatically:** line quantity × price, line sums, tax rates, totals and amount due are recomputed and compared with what is printed.
- **Catch misreads:** every extracted amount is cross-checked against the recognised document text, so a misread number is flagged instead of silently accepted.
- **Enforce your rules:** expected currency, allowed VAT rates, duplicates within the batch.
- **Get spreadsheet-ready files:** `invoices.csv` (one row per invoice) and `line-items.csv` (one row per line item).
- **Automate it:** call it from the API, a schedule, n8n, Make, Zapier or an AI agent via Apify MCP.

#### Checks and flags

| Flag | Severity | Meaning |
|---|---|---|
| `LINE_MATH_MISMATCH` | error | quantity × unit price ≠ line amount |
| `LINE_TAX_MISMATCH` | error | line tax ≠ line amount × line tax rate |
| `LINE_SUM_MISMATCH` | error | line amounts do not add up to the subtotal |
| `TAX_RATE_MISMATCH` | error | VAT summary: base × rate ≠ tax amount |
| `TAX_BREAKDOWN_MISMATCH` / `LINE_TAX_SUM_MISMATCH` | error | tax lines do not add up to the tax total |
| `TOTAL_MISMATCH` | error | subtotal − discount + shipping + tax ≠ total |
| `AMOUNT_DUE_MISMATCH` | error | total + previous balance − paid ≠ amount due |
| `LINE_VALUES_NOT_AS_PRINTED` | error | an extracted quantity, price or amount does not match the numbers printed in that table row |
| `VALUE_NOT_IN_DOCUMENT` | error | an extracted subtotal, tax total, total or amount due does not appear anywhere in the document text |
| `DUPLICATE_IN_BATCH` | error | same vendor, invoice number and total as an earlier invoice in the run |
| `CURRENCY_MISMATCH` | error | currency differs from `expectedCurrency` |
| `UNEXPECTED_TAX_RATE` | error | a tax rate not in `allowedTaxRates` |
| `DUE_DATE_BEFORE_INVOICE_DATE`, `INVALID_INVOICE_DATE` | error | date problems |
| `MISSING_INVOICE_NUMBER`, `MISSING_INVOICE_DATE`, `MISSING_VENDOR`, `MISSING_TOTAL` | error | required field not found |
| `MISSING_CURRENCY`, `NO_LINE_ITEMS`, `NOT_AN_INVOICE`, `INVOICE_DATE_IN_FUTURE`, `INVALID_DUE_DATE` | warning | worth a look |
| `PAGE_CAP_REACHED`, `TEXT_TRUNCATED` | warning | the document was longer than the limits below |

Amounts are compared with a tolerance of `amountTolerance` (default 0.01), plus half a cent per unit on quantity × price to allow for rounded unit prices. An invoice with any error flag gets `status: "needs_review"`; otherwise `status: "valid"`.

### How to use

1. Put direct links to your invoice files in **Invoice URLs** — one invoice per file (PDF, JPG, PNG, WEBP, GIF, TIFF or AVIF). The links must download without a login: an Apify key-value store record, a pre-signed S3 link or a public file host all work.
2. Optionally set the expected currency, the allowed VAT rates and the tolerance.
3. Start the run. Each invoice becomes one row in the **Output** tab; the CSV files are linked there too.

A one-page invoice takes about 5–10 seconds; up to 3 invoices are processed in parallel.

| Field | Required | Description |
|---|---|---|
| `invoiceUrls` | yes | Direct, publicly downloadable links. Up to 1,000 per run; repeated links are processed once. |
| `maxPagesPerInvoice` | no | 1–5, default 5. Only the first N pages of each PDF are read. |
| `expectedCurrency` | no | ISO code such as `EUR`. |
| `allowedTaxRates` | no | Tax/VAT rates in percent, e.g. `20`, `10`, `5.5`, `0`. |
| `amountTolerance` | no | Default `0.01`. |
| `includeOcrText` | no | Adds the recognised page text to each item for auditing. |

### Pricing

You pay per processed invoice. **Failed invoices (unreadable, not downloadable) are free.**

| Event | Price |
|---|---|
| Invoice processed (up to 5 pages) | $0.05 |

| Example run | Cost |
|---|---|
| 1 invoice | $0.05 |
| 100 invoices (a month of supplier bills) | $5.00 |
| 1,000 invoices (maximum per run) | $50.00 |

Before processing, the Actor checks that your run's maximum cost covers the whole batch ($0.05 × number of invoices). If it doesn't, the run stops without charging anything and tells you the amount to set.

Available on paid Apify plans.

### Input example

```json
{
  "invoiceUrls": [
    "https://example.com/invoices/INV-100.pdf",
    "https://example.com/scans/receipt-7.jpg"
  ],
  "expectedCurrency": "USD",
  "allowedTaxRates": ["10"]
}
```

### Output example

One dataset item per invoice:

```json
{
  "index": 0,
  "sourceUrl": "https://example.com/invoices/INV-100.pdf",
  "status": "needs_review",
  "flagCodes": ["LINE_MATH_MISMATCH"],
  "flags": [{ "code": "LINE_MATH_MISMATCH", "severity": "error", "message": "Line 1: 1 x 1 = 1.00, but the line shows 100.00." }],
  "vendorName": "Example Supplies Ltd.",
  "invoiceNumber": "INV-100",
  "invoiceDate": "2026-09-15",
  "currency": "USD",
  "total": 110,
  "amountDue": 610,
  "invoice": {
    "document_type": "invoice",
    "invoice_number": "INV-100",
    "invoice_date": "2026-09-15",
    "due_date": "2026-10-15",
    "currency": "USD",
    "vendor": { "name": "Example Supplies Ltd.", "address": "1 Sample Street, Springfield", "tax_id": null },
    "customer": { "name": "Demo Customer Inc.", "address": "99 Test Avenue, Riverside", "tax_id": null },
    "line_items": [{ "description": "Consulting services", "quantity": 1, "unit_price": 1, "amount": 100, "...": "..." }],
    "subtotal": 100, "tax_total": 10, "total": 110, "previous_balance": 500, "amount_due": 610, "...": "..."
  },
  "checks": { "lineItemsSum": 100, "lineTaxSum": null, "computedTotal": 110, "computedAmountDue": 610, "impliedTaxRate": 10 },
  "pagesProcessed": 1,
  "charged": true,
  "error": null
}
```

The run's key-value store also gets:

- `invoices.csv` — one row per invoice (header fields, totals, status, flag codes)
- `line-items.csv` — one row per line item, keyed by source URL and invoice number
- `OUTPUT` — run summary with counts and links to both CSV files

Files that cannot be downloaded or read come back with `status: "failed"` and an `error` message, and are not charged.

### Limits

- **Up to 5 pages per invoice.** Longer PDFs are cut after page 5 (or after `maxPagesPerInvoice`) and flagged `PAGE_CAP_REACHED`. Split multi-invoice PDFs into one file per invoice.
- About 30,000 characters of recognised text per invoice are used for extraction; beyond that the invoice is flagged `TEXT_TRUNCATED`.
- Up to 1,000 invoices per run, 3 processed in parallel.
- Links must be directly downloadable without a login.

### Good to know

- Values are read as printed; the Actor never "corrects" a number, it flags it. Always review `needs_review` invoices before paying them.
- Handwritten or very low-resolution scans may be read less reliably — the grounding checks will usually flag them.
- Download links to the CSV files expire with your plan's storage retention — save the files you need.
- Questions or a missing check? Open an issue on the Actor page.

# Actor input Schema

## `invoiceUrls` (type: `array`):

Direct links to invoice files: PDF, JPG, PNG, WEBP, GIF, TIFF or AVIF. Each link must be publicly downloadable (for example an Apify key-value store record, a pre-signed S3 link or a public file host). One invoice per file. Repeated links are processed once.

## `maxPagesPerInvoice` (type: `integer`):

Only the first N pages of each PDF are read (1 to 5). Pages after the cap are ignored and the invoice gets a PAGE_CAP_REACHED warning.

## `expectedCurrency` (type: `string`):

Optional ISO 4217 code such as EUR or USD. Invoices in another currency get a CURRENCY_MISMATCH flag.

## `allowedTaxRates` (type: `array`):

Optional list of tax rates in percent, for example 20, 10, 5.5, 0. Any other rate found on an invoice gets an UNEXPECTED_TAX_RATE flag.

## `amountTolerance` (type: `number`):

Largest difference (in invoice currency) accepted between a printed amount and the recomputed one before a mismatch is flagged.

## `includeOcrText` (type: `boolean`):

Add the recognised page text (Markdown) to each dataset item, for auditing the extraction.

## Actor input object example

```json
{
  "invoiceUrls": [
    "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/invoice.pdf"
  ],
  "maxPagesPerInvoice": 5,
  "amountTolerance": 0.01,
  "includeOcrText": false
}
```

# Actor output Schema

## `invoices` (type: `string`):

No description

## `invoicesCsv` (type: `string`):

No description

## `lineItemsCsv` (type: `string`):

No description

## `summary` (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 = {
    "invoiceUrls": [
        "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/invoice.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("seasons-ai/invoice-batch-validator").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 = { "invoiceUrls": ["https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/invoice.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("seasons-ai/invoice-batch-validator").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 '{
  "invoiceUrls": [
    "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/invoice.pdf"
  ]
}' |
apify call seasons-ai/invoice-batch-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,seasons-ai/invoice-batch-validator"
        }
    }
}
```

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/iyjIEFMk1cUV3lD5u/builds/qrN6q90E7RVDlFhff/openapi.json
