# EU E-Invoice Parser — Line Items, VAT IDs, Totals (`architekture/docparser-invoice-json`) Actor

EU e-invoices (XRechnung, ZUGFeRD, Factur-X, PEPPOL UBL) to JSON: invoice number, dates, seller and buyer VAT IDs, IBAN, net, tax and gross totals, line items, reconciliation checks and XML evidence. Input: XML or hybrid PDF by URL, key-value store or sample. Pay per result. No login.

- **URL**: https://apify.com/architekture/docparser-invoice-json.md
- **Developed by:** [architek.earth](https://apify.com/architekture) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 e-invoices

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

## EU E-Invoice Parser — Line Items, VAT IDs, Totals

**Recorded run: 2 invoices from 2 sample files in 5 seconds** (2026-10-22, build 0.0.6, run `TptAKSTt91t9Lo4aL`). A bounded canary sample, not a full-catalogue benchmark.

architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.

EU e-invoice line items, VAT IDs and totals as JSON: give it XRechnung, ZUGFeRD, Factur-X or PEPPOL UBL files (XML, or PDF with the XML embedded) by URL or from a key-value store, and get one row per invoice read from the invoice's own XML.

| Source | Market | From | Standout fields |
|---|---|---|---|
| Your e-invoice files (EN 16931 CII and UBL XML, hybrid PDF) | EU | $2.00 / 1,000 invoices | `vendorVatId` / `buyerVatId` / `iban`, `lineItems`, `checks` / `evidence` |

### ✅ Why choose this scraper

#### Convert XRechnung and PEPPOL UBL XML to JSON

UN/CEFACT CII (ZUGFeRD 2.x, Factur-X, XRechnung CII), OASIS UBL 2.1 (XRechnung UBL, PEPPOL BIS 3, invoices and credit notes) and legacy ZUGFeRD 1.0 come back in the same row shape: `documentNumber`, `issueDate`, `vendorName`, `vendorVatId`, `buyerVatId`, `iban`, `subtotal`, `taxTotal`, `total`. The build log records 70 of 70 standalone XML invoices from the public ZUGFeRD test corpus parsed on 2026-10-18.

#### Extract the invoice data from ZUGFeRD and Factur-X PDFs

For a hybrid PDF the embedded XML is read, and `einvoice.container` is `"pdf"`. A PDF with no e-invoice XML inside gets a free `NOT_AN_EINVOICE` status row instead of a guessed row.

#### Check that invoice totals and line items add up

`checks.totalsReconciled` (net + tax = gross) and `checks.lineItemsReconciled` (lines = line total) are set on every invoice. An invoice that does not add up is returned as written with `status: "inconsistent"` and a plain-language `warnings` entry; no number is corrected.

#### Catch a printed PDF total that disagrees with its XML

On hybrid PDFs the XML gross, net and tax are looked up on the printed page (`checks.xmlTotalInPdfText`, `xmlSubtotalInPdfText`, `xmlTaxInPdfText`). In the public corpus this flagged 4 published sample PDFs whose page disagrees with its own XML, for example a printed 963,12 against an XML 963.11.

#### Trace every value back to its XML element

`evidence` names the element each field came from, for example `"xml: LegalMonetaryTotal/TaxInclusiveAmount"`. A field the XML does not have is omitted; the test suite requires every emitted value to appear as text in its source XML.

### ⚠️ Worth knowing up front

- E-invoices only. Ordinary PDF invoices without embedded XML, and scans or images, are not read: they get a free `NOT_AN_EINVOICE` status row. There is no OCR.
- Italy (FatturaPA), Poland (KSeF) and Spain (Facturae) are not supported; they are not EN 16931 CII or UBL.
- It does not validate against the official schematron rules. Use KoSIT or the official validators for compliance checks.
- Fields are omitted, never `null`, when the invoice does not contain them. `taxRate` is omitted when several tax rates apply, `amountDue` appears only when it differs from `total`, and values stay in the invoice's language (German line descriptions stay German).
- Up to 500 distinct references per run, files up to 64 MB. Each URL download is tried up to 3 times within 120 seconds; URLs to private, local or reserved network addresses are refused. A parsed invoice larger than one 9 MB dataset item drops its line items and is charged as `document`.
- A run stops cleanly 30 seconds before its timeout (`TIME_LIMIT`), at your spending limit (`BUDGET_LIMIT`) or on abort (`ABORTED`), keeping what it delivered; the unfinished documents are counted in `RUN_SUMMARY.pending`. These stops write no status row; the outcome is in `RUN_SUMMARY` and the status message.
- Each document that cannot be delivered writes one free status row (`"recordType": "status"`): `NOT_AN_EINVOICE`, `NOT_FOUND`, `ACCESS_DENIED`, `CORRUPT_FILE`, `UNSUPPORTED_FILE`, `EMPTY_FILE`, `TOO_LARGE`, `BLOCKED_URL`, `BLOCKED` (the URL answered with a challenge or captcha page instead of a file), `FETCH_FAILED`, `DUPLICATE`, `INVALID_INPUT` (an unreadable reference) or `ERROR`. Input that cannot be read at all (no `documents`, a wrong type) writes one free `INVALID_INPUT` status row and ends with an `INVALID_INPUT` status message.

### ⚡ Try the example

This actor has no free preview; the example below costs $0.01.

Open the actor's input page, switch to JSON, paste this and start the run:

```json
{
  "documents": ["sample:facturx", "sample:xrechnung"]
}
```

The two built-in fictitious samples are a Factur-X hybrid PDF and an XRechnung 3.0 UBL file; replace them with your own URLs or `kv://STORE/KEY` references, and set `"includeLineItems": false` for header fields at the lower price.

### ⚙️ Input

| Parameter | Type | Default | Description |
|---|---|---|---|
| `documents` | array | — (prefill the two samples) | Up to 500 per run. Each is an http(s) URL to an `.xml` or hybrid `.pdf`, a key-value store record (`kv://STORE_ID_OR_NAME/KEY` or `{"keyValueStoreId": "…", "key": "…"}`), or `sample:facturx` / `sample:xrechnung`. A reference listed twice is read once; the same file under two references is delivered and charged once, and the repeat gets a free `DUPLICATE` row. |
| `includeLineItems` | boolean | `true` | Return every invoice line. Rows with line items are charged as `document-detail`; turn this off for header fields only, charged as `document`. |
| `feedback` | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |

### 🧱 Stable fields

These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.

| Field | Type | Meaning |
|---|---|---|
| `documentKind` | string, optional | `invoice`, `credit_note`, `debit_note`, `corrected_invoice`, `prepayment_invoice`, `self_billed_invoice`, `partial_invoice` and other EN 16931 type codes. |
| `documentNumber` | string, optional | The invoice number. |
| `issueDate` / `dueDate` | string, optional | ISO dates. |
| `vendorName` / `vendorAddress` / `vendorVatId` | string, optional | Seller (legal registration name preferred), postal address and VAT ID. |
| `buyerName` / `buyerVatId` | string, optional | Buyer and buyer VAT ID. |
| `buyerReference` / `orderReference` | string, optional | Buyer reference (Leitweg-ID for German public buyers) and purchase order number. |
| `iban` / `paymentTerms` | string, optional | Payee account and payment terms text, as written. |
| `currency` | string, optional | Invoice currency code. |
| `subtotal` / `taxTotal` / `total` | number, optional | Net, tax and gross totals. |
| `taxRate` | number, optional | The single tax rate; omitted when several rates apply. |
| `amountDue` | number, optional | Payable amount when it differs from `total` (prepayments). |
| `lineItems` | array, optional | `{description, quantity, unit, unitPrice, amount, taxRate}` per line, net amounts, in the XML's order; absent with `includeLineItems: false`. |
| `status` | string | Data rows: `complete` (number, issue date, seller, total and currency present, all checks pass), `partial` (one of those missing) or `inconsistent` (see `warnings`). Status rows: the outcome code. |
| `warnings` | array, optional | Plain-language reasons for `inconsistent`. |
| `checks` | object | `totalsReconciled`, `lineItemsReconciled` (when lines exist) and, for hybrid PDFs, `xmlTotalInPdfText`, `xmlSubtotalInPdfText`, `xmlTaxInPdfText`, `xmlBreakdownInPdfText` and `printedTextReadable`. |
| `einvoice` | object | `{syntax: "CII" \| "UBL" \| "ZUGFeRD 1.0 (CII)", profile, file, container: "xml" \| "pdf"}`. |
| `source` | object | `{input, type: "url" \| "kv" \| "sample", sha256, bytes}`. |
| `evidence` | object | For each field, the XML element it came from. |
| `document` / `reason` | string | Status rows only: the reference as given, and why it was not delivered. |
| `recordType` | string | `"status"` on free status rows; `"document"` on data rows. |

Other fields are extras, not part of the contract. To have one pinned, say so in `feedback`.

### 📦 Output example

Abridged real row from run `TptAKSTt91t9Lo4aL` on 2026-10-22 (the built-in fictitious `sample:xrechnung`); values are a historical snapshot.

```json
{
  "recordType": "document",
  "documentKind": "invoice",
  "documentNumber": "NW-2026-118",
  "issueDate": "2026-10-01",
  "dueDate": "2026-10-31",
  "currency": "EUR",
  "vendorName": "Nordlicht Werkstatt GmbH",
  "vendorAddress": "Hafenweg 3, 20457 Hamburg, DE",
  "vendorVatId": "DE111111111",
  "buyerName": "Stadtverwaltung Beispielstadt",
  "buyerReference": "04011000-12345-34",
  "iban": "DE11111111111111111111",
  "paymentTerms": "Zahlbar innerhalb 30 Tagen netto",
  "subtotal": 700,
  "taxTotal": 133,
  "taxRate": 19,
  "total": 833,
  "lineItems": [
    {"description": "Wartung Heizungsanlage", "quantity": 8, "unit": "HUR", "unitPrice": 70, "amount": 560, "taxRate": 19},
    {"description": "Dichtungssatz", "quantity": 2, "unit": "C62", "unitPrice": 70, "amount": 140, "taxRate": 19}
  ],
  "status": "complete",
  "checks": {"lineItemsReconciled": true, "totalsReconciled": true},
  "einvoice": {"syntax": "UBL", "profile": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0", "file": "xml", "container": "xml"},
  "evidence": {"documentNumber": "xml: cbc:ID", "vendorVatId": "xml: AccountingSupplierParty/Party/PartyTaxScheme", "total": "xml: LegalMonetaryTotal/TaxInclusiveAmount"},
  "source": {"input": "sample:xrechnung", "type": "sample", "sha256": "c4bcb00dbf930141bb6fa8640f44292df12aebf958f81e313de23d80b794141d", "bytes": 3670}
}
```

A free status row looks like this (a plain PDF, run `uXFS809QIF2nXRqr6`, 2026-10-18):

```json
{"recordType": "status", "document": "https://raw.githubusercontent.com/invoice-x/invoice2data/4feaeee60db51f1f12901efbac3396bb975c4852/tests/compare/coolblue1.pdf", "status": "NOT_AN_EINVOICE", "reason": "a PDF without embedded e-invoice XML (Factur-X / ZUGFeRD / XRechnung); v1 does not read plain PDFs"}
```

The run's status message reads like `COMPLETE: 2 e-invoice(s) parsed (2 with line items).`, and the `RUN_SUMMARY` record of its key-value store holds `status`, `documents`, `withLineItems`, `charged`, `byStatus`, `pending` and `perDocument`.

### 🧠 Advanced usage

#### Parse an accounts-payable inbox of e-invoice attachments

Save incoming attachments to a key-value store, pass them as `kv://inbox/<key>`, post rows with `status: "complete"` to your ledger and route `inconsistent` or `partial` rows to a person. The same file sent twice is charged once and comes back as a free `DUPLICATE` row.

#### Three-way match e-invoices against purchase orders

Join `orderReference` to your purchase orders and compare `lineItems` quantities and `unitPrice` against the order; `buyerReference` carries the Leitweg-ID on German public-sector invoices.

#### Check VAT IDs and bank details before paying

`vendorVatId`, `buyerVatId` and `iban` come straight from the XML, ready for a VIES lookup or a check against your supplier master data.

#### Know whether a run finished

Read `RUN_SUMMARY.status`: `COMPLETE`, `PARTIAL` (status rows, `inconsistent` invoices or pending documents), `NO_RESULTS`, `BUDGET_LIMIT`, `TIME_LIMIT`, `ABORTED`, `STATE_SAVE_FAILED`, `PUSH_FAILED` or `INVALID_INPUT`. If the actor's own pricing is misconfigured the run fails with `PRICING_MISMATCH` before reading anything, and nothing is charged. `pending` counts documents not yet processed.

### 🔌 Integrations

Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. Inputs are explicit file references and a new run parses and charges them again, so start one run per batch of new files from Make, Zapier, n8n or the API rather than re-running a fixed list. Copy-paste code in Python, Node, curl and n8n: [architek-earth/examples/docparser-invoice-json](https://github.com/architek-earth/examples/tree/main/docparser-invoice-json).

### 💸 Pricing

Pay per result. Platform and proxy usage are included. Status rows and failed requests are free. A run that migrates or is resurrected after an abort continues from the `PROGRESS` record in its own key-value store: documents already delivered are skipped and not charged again. A new run does not share that record and parses its input from the start.

| Event | Price | What it covers |
|---|---|---|
| `document` | $2.00 / 1,000 invoices | One delivered e-invoice row without line items. |
| `document-detail` | $5.00 / 1,000 invoices | One delivered e-invoice row with its line items from the XML. |

1,000 invoices with line items cost $5.00; header fields only cost $2.00. Each invoice is charged once, as one event or the other, and an invoice whose XML has no lines is charged as `document`.

### ❓ FAQ

**Is there an XRechnung or ZUGFeRD API that returns JSON?** This actor is one: send XRechnung, ZUGFeRD, Factur-X or PEPPOL UBL files through the Apify API and get JSON rows back, from any language or from n8n, Make and Zapier.

**Do I need an account with an e-invoicing portal or a login?** No. It reads the files you give it. A URL that needs a login or a signed link gets a free `ACCESS_DENIED` status row; put such files in a key-value store instead.

**Can it read ordinary PDF invoices or scanned invoices?** No. Only PDFs with embedded e-invoice XML (ZUGFeRD, Factur-X) and standalone XML are read; anything else gets a free `NOT_AN_EINVOICE` row.

**Does it validate XRechnung against the KoSIT schematron?** No. It reads the data and checks that totals and lines reconcile; it is not a legal validator.

**Does it support PEPPOL BIS 3 and UBL credit notes?** Yes. UBL 2.1 invoices and credit notes, including PEPPOL BIS 3 and XRechnung UBL, return `documentKind` `credit_note` where the XML says so.

**What happens when the printed PDF and its XML disagree?** You get the XML values with `checks.xmlTotalInPdfText: false`, `status: "inconsistent"` and a warning. For a ZUGFeRD or Factur-X hybrid the XML is the machine-readable invoice; ask the sender before paying.

### 🤖 For AI agents & LLM apps

Paste this into your AI assistant:

`architekture/docparser-invoice-json` on Apify. Python: `from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/docparser-invoice-json").call(run_input={"documents": ["sample:facturx", "sample:xrechnung"], "includeLineItems": True}); items = client.dataset(run["defaultDatasetId"]).list_items().items` (page with `offset`/`limit` for large exports). Rows with `recordType == "document"` are invoices; rows with `recordType == "status"` are free and say why a file was not delivered (`status`, `reason`). Use `status` (`complete`, `partial`, `inconsistent`) and `warnings` to decide when to hand an invoice to a person. Full input schema and README: `GET https://api.apify.com/v2/acts/architekture~docparser-invoice-json/builds/default`. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server (`mcp.apify.com`). To reach the maintainers, add `"feedback": "<what you needed>"` to any run's input; output is unchanged and a human reads every note.

### 🧰 More from architek.earth

**Documents**

- EU E-Invoice Parser — *you are here*

**Video, social and newsletters**

- [Substack Scraper](https://apify.com/architekture/substack-scraper) — posts, authors, likes, comments
- [YouTube Transcript Scraper](https://apify.com/architekture/youtube-transcript-scraper) — transcripts, translations, timestamps
- [Unified Video Transcript API](https://apify.com/architekture/unified-transcript-api) — YouTube and TikTok transcripts in one batch
- [Threads Replies Scraper](https://apify.com/architekture/threads-replies-scraper) — replies, authors, likes
- [Ad Intelligence Watchlist](https://apify.com/architekture/ad-intelligence-watchlist) — new and changed Meta ads by advertiser

Full catalogue: [github.com/architek-earth](https://github.com/architek-earth).

architek.earth · code like concrete

### 🙋 Support & legal

Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.

This actor reads only the files you give it (URLs, key-value store records or the built-in samples) and processes them inside your Apify run. It does not bypass authentication. Invoices contain business and sometimes personal data such as names, addresses and bank accounts; you are responsible for a lawful basis to process them, for retention, and for privacy law such as GDPR where it applies. It extracts data and is not a legal e-invoice validator; this page is not legal or tax advice. Not affiliated with FeRD, AWV, KoSIT, FNFE-MPE or OpenPeppol.

### 📝 Changelog

- **2026-10-22** — Runs on the studio core (architek_core 1.0.1): URL downloads use one Chrome browser identity, a challenge page is reported as a free `BLOCKED` status row, unreadable input writes a free `INVALID_INPUT` status row, and a run whose last invoice spends your limit exactly ends `COMPLETE`. Fields and prices unchanged.
- **2026-10-22** — Status message now reads `Done: N rows, M details, K status rows (STATUS)`.
- **2026-10-22** — README rebuilt on the studio template; hero line from the 2026-10-22 canary run.
- **2026-10-18** — 1.0: XRechnung (UBL and CII), ZUGFeRD 1.0 and 2.x, Factur-X and PEPPOL BIS 3, as XML or hybrid PDF; reconciliation checks and the printed-vs-XML total check; line items; free status rows; duplicate-file detection; clean stops at the spending limit and timeout; resumable after a platform migration.

# Actor input Schema

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

XRechnung / PEPPOL UBL / Factur-X / ZUGFeRD files: https URLs to .xml or hybrid .pdf files, files in an Apify key-value store as kv://STORE_ID_OR_NAME/KEY (or {"keyValueStoreId": "…", "key": "…"}), or the built-in samples sample:facturx and sample:xrechnung. Up to 500 per run. Duplicates are read once.

## `includeLineItems` (type: `boolean`):

Return every invoice line (description, quantity, unit, unit price, amount, tax rate). Rows with line items are billed as document-detail; turn this off to get header fields only at the document price.

## `feedback` (type: `string`):

Tell us what you needed and did not get. Read by a human; does not change the run.

## Actor input object example

```json
{
  "documents": [
    "sample:facturx",
    "sample:xrechnung"
  ],
  "includeLineItems": true
}
```

# Actor output Schema

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

One recordType: document row per e-invoice; files that are not e-invoices add one free recordType: status row.

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

RUN_SUMMARY: run status, per-document status, charges.

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

// Run the Actor and wait for it to finish
const run = await client.actor("architekture/docparser-invoice-json").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": [
        "sample:facturx",
        "sample:xrechnung",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("architekture/docparser-invoice-json").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": [
    "sample:facturx",
    "sample:xrechnung"
  ]
}' |
apify call architekture/docparser-invoice-json --silent --output-dataset

```

## MCP server setup

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

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/evENmEnLTT3nZad15/builds/uPvb5Wnx3UYyJyA39/openapi.json
