Logo
  • English icon
  • German icon
Article

E-invoices and PDF invoices, processed automatically: read exactly, checked before they are saved

dotnete-invoicingxrechnungzugferdopenaipostgresql

Since January 1, 2025, every company in Germany must be able to receive e-invoices from other businesses. From 2027, larger companies must also send them, from 2028 all companies. Until then, PDF invoices keep coming, some of them scanned. So incoming invoices stay mixed for years.

This article shows how an application processes both automatically, without wrong numbers ending up in the system. It is my own reference project; the source code is public. As of October 2026, not tax advice.

Key points

  • E-invoices need no AI. XRechnung and ZUGFeRD already contain all invoice data in a structured form. They are read exactly, in milliseconds.
  • AI only for PDFs without e-invoice data, including scans. The model returns the data in a fixed JSON schema, exactly as printed.
  • Every invoice is checked before it is saved, wherever it comes from: line arithmetic, totals, VAT per rate. If an AI result does not add up, the application reads the document a second time. If it still does not add up, the invoice is not saved, and the reason is shown.
  • Duplicates are recognized across formats: the same invoice as XRechnung and as a PDF by email.

The German timeline

DateWhat changes (invoices between businesses in Germany)
January 1, 2025Every company must be able to receive e-invoices
January 1, 2027Companies with more than €800,000 prior-year revenue must send e-invoices
January 1, 2028All companies must send e-invoices

Until the end of the transition rules, paper and PDF invoices remain allowed. XRechnung (pure XML) and ZUGFeRD (a PDF with the same data embedded as XML) are the common German formats under the European standard EN 16931.

One path for every file

Every file takes the same path: read, check, store.

  1. XRechnung (UBL or CII) is parsed directly from the XML.
  2. ZUGFeRD / Factur-X: the application finds the XML embedded in the PDF and reads it the same way. No AI, no guessing.
  3. Any other PDF, including scans without a text layer, goes to the AI (OpenAI, PDF file input, structured outputs).
  4. Every result goes through the same check. Only then is it written to PostgreSQL, with the original file in an archive and an entry in the processing log.

In a test run with seven fictitious invoices (two XRechnung, one ZUGFeRD, three regular PDFs, one scan), the e-invoices were done in milliseconds; each PDF took a few seconds. Result: five imported, one duplicate, one rejected. That is exactly how it should be.

One reader per format

Each format has its own reader behind one interface. A new format is simply another reader:

public interface IInvoiceReader
{
bool CanRead(InvoiceDocument document);
Task<ExtractionResult> ReadAsync(
InvoiceDocument document, CancellationToken ct);
}

The repository has three: XmlInvoiceReader (XRechnung UBL and CII), ZugferdPdfReader (ZUGFeRD and Factur-X) and OpenAiPdfReader (PDFs and scans).

A fixed format for the AI

The model has to return exactly these fields, in strict mode, so the answer always matches the schema. An excerpt:

"lines": [{
"quantity": { "type": "number" },
"free_quantity": { "type": "number" },
"unit_price": { "type": "number" },
"discount_percent": { "type": ["number", "null"] },
"net_amount": { "type": "number" },
"vat_rate": { "type": ["number", "null"] }
}]

The most important line of the instructions:

Return exactly the values printed on the document. Never calculate, correct or guess values. If printed totals do not add up, still return them as printed.

The model may not calculate or correct anything. Only then can the check find errors afterwards. In the test run the AI recognized line items, free quantities, discounts and two VAT rates (19 % for drinks, 7 % for coffee), and on the scan a bonus quantity listed as its own line.

The check

The AI reads well, but not without errors. And printed invoices can be wrong too. So the application recalculates every invoice:

  • per line: quantity × price − discount = line amount
  • sum of the lines (minus allowances, plus charges) = net amount
  • net + VAT = gross
  • VAT per rate, also with several rates on one invoice
public static decimal ExpectedLineAmount(InvoiceLine line)
{
var gross = line.Quantity * line.UnitPrice;
var discount = line.DiscountAmount
?? (line.DiscountPercent is { } percent ? Round(gross * percent / 100m) : 0m);
return Round(gross - discount);
}

If an AI result fails the check, the document is read a second time; a misread digit usually does not repeat. Deterministic readers (XML) are not read again. If the invoice still does not add up, it is not saved, and the reason is shown, for example: sum of the lines 456.70 ≠ net amount 465.70. In the test run that was a test invoice with a deliberately misprinted net amount, rejected also on the second read.

Duplicates

  • The same file again is recognized by its SHA-256 fingerprint before the AI is even asked.
  • The same invoice in another format, for example an XRechnung and the same invoice again as a PDF by email, is recognized by the supplier's VAT ID and the invoice number.

Limits and data protection

  • Very small print on a scan is not always read correctly. The check protects the amounts, but an unusual address should be looked at by a person.
  • Only PDFs without e-invoice data go to the AI. XRechnung and ZUGFeRD stay in-house. Requests are sent with store = false. Whether invoice data may go to an external service is your decision.

Under the hood

C# and .NET 10 with a WPF interface, PostgreSQL with Npgsql and Dapper, ZUGFeRD-csharp for XRechnung and ZUGFeRD, PdfPig for embedded XML, the OpenAI .NET SDK. All libraries are open source; unit and integration tests run on every change in GitHub Actions. Architecture decisions are documented in the repository (DECISIONS.md).

About the project

This is my own reference project with fictitious test data, not a client project. I have been building software with .NET for more than eight years, in recent years backends and integrations in the energy sector. If you want to automate your invoice processing or connect it to your system, get in touch.

Sources

Questions about this topic?

If you want to discuss this topic for your own environment, or need support with .NET, Azure or Dynamics 365, I'm happy to talk.

Get in touch