Automating E-Invoicing Compliance with Open-Source NLP
For developers and ML engineers, this regulatory momentum opens a genuinely interesting problem space: how do you take the messy, inconsistent pile of PDFs and semi-structured documents that most businesses currently call "invoices" and build a pipeline that extracts, validates, and converts them into structured XML that satisfies EN 16931?
This post walks through a practical open-source NLP approach using models available on the Hugging Face Hub, from document understanding with LayoutLMv3 to field validation with a fine-tuned token classifier.
Why PDFs Are Not E-Invoices
Before diving into the architecture, it helps to be precise about what "e-invoicing compliance" actually means technically.
A PDF invoice is a visual artifact. It encodes a document as a rendered image (or selectable text in a bounding box layout), designed so a human can read it. It tells you nothing about which string is the VAT number versus the invoice number unless you already know the visual grammar of that specific supplier's template.
A compliant e-invoice under EN 16931 is a structured data file, typically XML in UBL 2.1 or CII format, where every field is semantically labelled. The cbc:TaxAmount element is not just a number floating somewhere near the word "VAT"; it is a machine-readable declaration with a currency attribute and a taxonomy code. Accounting software on the receiving end can ingest, validate, and reconcile it without any human in the loop.
The gap between these two worlds is exactly where NLP becomes useful. A large fraction of invoices in circulation today exist only as PDFs or scanned images. Building a compliant pipeline means teaching a model to understand the layout and semantics of those documents well enough to produce a clean structured output.
The Stack
The approach described here uses three open-source components:
- LayoutLMv3 for document understanding and field extraction
- A sequence classification head for document-level routing and validation
- A rule-based XML renderer to assemble the EN 16931 output
All models are loaded via
transformersand all weights are pulled from the Hub.
from transformers import AutoProcessor, AutoModelForTokenClassification
import torch
processor = AutoProcessor.from_pretrained(
"microsoft/layoutlmv3-base", apply_ocr=True
)
model = AutoModelForTokenClassification.from_pretrained(
"Theivaprakasham/layoutlmv3-finetuned-invoice"
)
Theivaprakasham/layoutlmv3-finetuned-invoice is already fine-tuned on an invoice dataset and predicts labels such as INVOICE_NUMBER, INVOICE_DATE, BILLER_NAME, BILLER_ADDRESS, DUE_DATE, SUBTOTAL, and TOTAL, which map directly onto required EN 16931 fields.
LayoutLMv3: Why Layout Matters
Standard text-only models struggle with invoices because proximity and spatial position carry meaning that does not survive plain tokenisation. "120.00" appears seventeen times in a purchase order. Which instance is the line-item total, which is the VAT amount, and which is the unit price? A human knows because of where it sits on the page, not because of any surrounding words.
LayoutLMv3 jointly encodes text tokens, their bounding box coordinates, and a patch-based visual representation of the document image. On the FUNSD benchmark, LayoutLMv1 already pushed F1 from 60% (BERT baseline) to 90%. LayoutLMv3 incorporates visual features during pre-training and improves further, as described in the Accelerating Document AI post on the HF blog.
The model takes as input:
from PIL import Image
image = Image.open("invoice.png").convert("RGB")
encoding = processor(image, return_tensors="pt")
with torch.no_grad():
outputs = model(**encoding)
predictions = outputs.logits.argmax(-1).squeeze().tolist()
token_boxes = encoding.bbox.squeeze().tolist()
tokens = processor.tokenizer.convert_ids_to_tokens(
encoding["input_ids"].squeeze().tolist()
)
Each predicted label aligns with a token and its bounding box, giving you both the extracted value and its position in the document.
Building the Extraction Pipeline
Once you have token-level predictions, you reassemble them into field-level extractions. Invoice fields are often multi-token spans (addresses, line item descriptions), so you apply a simple BIO merging pass:
def merge_bio_predictions(tokens, predictions, label_map):
entities = []
current_entity = None
for token, pred in zip(tokens, predictions):
label = label_map[pred]
if label.startswith("B-"):
if current_entity:
entities.append(current_entity)
current_entity = {"label": label[2:], "tokens": [token]}
elif label.startswith("I-") and current_entity:
current_entity["tokens"].append(token)
else:
if current_entity:
entities.append(current_entity)
current_entity = None
return [
{"label": e["label"], "value": " ".join(e["tokens"]).replace(" ##", "")}
for e in entities
]
This gives you a clean dictionary of extracted fields ready for validation.
Validation: Catching What the Model Misses
Extraction accuracy is not enough on its own. An EN 16931-compliant invoice must contain specific mandatory fields, and those fields must pass format checks before the XML is emitted. A separate validation step using regex patterns and field-presence checks prevents malformed invoices from reaching the output pipeline.
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class InvoiceFields:
invoice_number: Optional[str] = None
invoice_date: Optional[str] = None
seller_vat: Optional[str] = None
buyer_vat: Optional[str] = None
total_amount: Optional[str] = None
vat_amount: Optional[str] = None
currency: str = "EUR"
VAT_PATTERN = re.compile(r"^[A-Z]{2}[0-9A-Z]{2,12}$")
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def validate_invoice(fields: InvoiceFields) -> list[str]:
errors = []
if not fields.invoice_number:
errors.append("Missing mandatory field: invoice_number")
if not fields.invoice_date or not DATE_PATTERN.match(fields.invoice_date):
errors.append("Invalid or missing invoice_date (expected ISO 8601)")
if fields.seller_vat and not VAT_PATTERN.match(fields.seller_vat):
errors.append(f"Invalid seller VAT format: {fields.seller_vat}")
if not fields.total_amount:
errors.append("Missing mandatory field: total_amount")
return errors
VAT number format validation is particularly important in a cross-border European context. The regex above handles the basic EU format; a production system would additionally call a VIES lookup for live verification.
Rendering EN 16931-Compliant XML
With validated fields in hand, the final step renders the structured output. The UBL 2.1 schema is verbose but deterministic:
from lxml import etree
def render_ubl_invoice(fields: InvoiceFields) -> bytes:
nsmap = {
None: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
"cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
}
root = etree.Element("Invoice", nsmap=nsmap)
cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
etree.SubElement(root, f"{{{cbc}}}CustomizationID").text = (
"urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
)
etree.SubElement(root, f"{{{cbc}}}ID").text = fields.invoice_number
etree.SubElement(root, f"{{{cbc}}}IssueDate").text = fields.invoice_date
etree.SubElement(root, f"{{{cbc}}}InvoiceTypeCode").text = "380"
etree.SubElement(root, f"{{{cbc}}}DocumentCurrencyCode").text = fields.currency
return etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="UTF-8")
The CustomizationID value signals Peppol BIS Billing 3.0 conformance, which is the profile used for Peppol network delivery across the Netherlands, Belgium, Germany, and most of the EU.
Fine-Tuning on Your Own Invoice Dataset
The Theivaprakasham/layoutlmv3-finetuned-invoice checkpoint is a strong starting point, but invoice layouts vary significantly across suppliers, countries, and ERPs. If your use case involves a narrow document type (say, Dutch government supplier invoices or NLCIUS-formatted exports), fine-tuning on a small labelled dataset of your own invoices usually produces substantial gains.
The Hub's datasets library makes it straightforward to structure annotation data for token classification:
from datasets import Dataset
# Each example: image path, word tokens, bounding boxes, NER labels
dataset = Dataset.from_dict({
"image": [...],
"words": [...],
"boxes": [...],
"ner_tags": [...],
})
A few hundred labelled examples are typically sufficient for a domain-specific fine-tune, especially when starting from a strong document-understanding checkpoint. Annotation can be done with tools like Label Studio or Prodigy, both of which export formats compatible with the token classification training loop.
Where This Fits in a Real Compliance Workflow
The NLP extraction pipeline described here handles the hardest part of the problem: converting unstructured or semi-structured source documents into validated structured data. But a production compliance workflow also needs:
- Peppol connectivity to deliver invoices over the four-corner network
- VIES VAT validation for cross-border buyer/seller verification
- Archiving in accordance with national retention rules (7 years in the Netherlands)
- Error handling and human review queues for low-confidence extractions
The good news is that the NLP layer is cleanly separable from the delivery and archival infrastructure. The model produces a validated
InvoiceFieldsobject; downstream systems consume it. This makes it straightforward to slot into existing ERP integrations or financial automation platforms.
Structured by Design, Compliant by Default
E-invoicing compliance is arriving across Europe on a defined legislative timeline. For developers, it is a concrete and solvable document AI problem with real stakes: bad extraction means invalid invoices, payment delays, and eventually regulatory penalties.
The Hugging Face ecosystem provides the key building blocks. LayoutLMv3 handles the layout-aware field extraction that text-only models cannot reliably perform. Fine-tuned checkpoints on the Hub accelerate time to a working baseline. The validation and XML rendering layers are standard Python. And as the EU harmonises around EN 16931 and Peppol, the target output format is well-defined and stable.
If you are working on invoice processing, document AI, or financial compliance automation, this is a good moment to build. The models are ready; the regulatory window is open.

