We had to solve a problem that a lot of German and EU companies will hit over
the next 18 months: a mid-sized company writes its invoices in Word, and from
2027 it legally has to send structured e-invoices — ZUGFeRD for businesses,
XRechnung for public buyers. Replacing their invoicing process was not an
option. So we put a workflow behind it instead.
What it does
IMAP inbox per department → split PDF attachments → archive the source PDF →
extract text → parse it with rules → build an EN 16931 dataset and recompute
the totals → validate → approve → generate the e-invoice → store → send.
22 nodes. Attached below as JSON with a dummy company and a generic parser.
Why no LLM for the parsing
The invoices come out of one Word template. Same layout every time, item table
in the same place, totals block in the same place. For that, a rule-based
parser is not the lesser option — it is the right one: same input, same output,
every run. No per-call cost, no external service in the path of invoice data,
and when it gets something wrong you can see why.
The honest trade-off: if the customer changes the template, the parser breaks.
That is not a bug, it is the deal you make. An LLM earns its place on incoming
invoices from many different senders — a different problem entirely.
What is not on a PDF invoice
Two things you will need and will not find in the document: the buyer’s email
address and, for public-sector invoices, the Leitweg-ID. We solved it with a
customer table as a mandatory input to the workflow. Same for bank details and
the contact person per department. Plan for that table from the start — it is
not an afterthought.
Three validation stages
- Inside the workflow — totals must match within ±2 cents, required fields
present, customer exists in the table. /validatewith thevalidflag — object-level rules./validatewithresults[]per format — XSD and Schematron against
XRechnung and ZUGFeRD.
The 7 mistakes
All of these were mine, and every single one only showed up on the first run
with real documents:
type: "creditnote"— it iscredit_note.- Left out
dueDatebecause the PDF had no due date. It is mandatory;
falling back to the invoice date is fine. - Forgot
countrySpecific.countryCode: "DE"— without it the Leitweg-ID is
not carried into the output. - Put the bank details in
paymentMeans. They belong under
seller.bankAccount. - No contact person. BR-DE-2 requires
seller.contact. - Bank details alone are not enough — BR-DE-1 also wants
paymentMethods: [{ type: "bank_transfer" }]. - Credit note with negative line items. The sign is carried by the document
type, the amounts stay positive (BR-27).
Two things that cost me an hour each
One PDF, two invoices. Extract text per page, not per document, and send
each page to the parser separately. Empty pages do no harm.
Duplicate detection. Build your own fingerprint over the invoice data with a
stable key order. Do not use a byte-level hash of the request — it changes
between calls even for identical content.
What this workflow does not do
- No OCR. Scanned PDFs without a text layer will not work.
- Template changes break the parser (see above).
- No duplicate protection across separate runs.
- No human approval step. It is easy to add, it is just not in there.
Nodes used
IMAP Email Trigger, Code, HTTP Request, Read/Write Files From Disk, Switch,
IF, and the community node n8n-nodes-invoice-api-xhub (MIT, currently 1.2.1)
for building and validating the e-invoice.
Disclosure: I am one of the makers of that community node. The workflow
itself is generic — the parsing, the page splitting and the fingerprint work
with any e-invoicing backend you prefer.
Happy to answer questions about the parsing or the EN 16931 side of it. And if
anyone has solved the OCR case for scanned invoices in n8n, I would like to
hear how.