How to validate a Peppol BIS 3.0 invoice
By evoicely · Last checked 20 October 2026
To validate a Peppol BIS Billing 3.0 invoice, you check the UBL XML against three sets of rules: the UBL 2.1 schema, the EN 16931 business rules, and OpenPeppol's own rules (PEPPOL-EN16931-* and PEPPOL-COMMON-*, plus country rules). OpenPeppol publishes all of them as Schematron files with each BIS release. Your sending access point runs the same checks before it lets a document into the network, so an invoice that fails here won't be delivered. For a quick check without installing anything, upload the file to our free e-invoice validator. It applies the official rule files, lists every error with its rule ID, and doesn't store your file.
What "valid" means in Peppol
A Peppol BIS Billing 3.0 invoice is a UBL 2.1 Invoice or CreditNote that follows the European standard EN 16931 and OpenPeppol's restrictions on top of it. The checks come in layers, and the prefix of a rule ID tells you which layer flagged it:
| Layer | What it checks | Rule IDs |
|---|---|---|
| XML schema | Structure: element names, order, nesting and data types (UBL 2.1). | Schema errors with a line number |
| EN 16931 | The European standard's business rules: required fields, totals that add up, VAT categories, code lists. Published by CEN. | BR-*, BR-CO-*, BR-S-*, BR-AE-*, BR-CL-*, UBL-* |
| Peppol BIS | OpenPeppol's rules for every Peppol invoice: business process, references, electronic addresses, no empty elements, code lists. | PEPPOL-EN16931-R*, -P*, -CL* |
| Identifiers | Format and check digits of party identifiers by scheme, such as the Belgian enterprise number (0208) or a GLN (0088). | PEPPOL-COMMON-R* |
| Country rules | Extra rules that only fire when the seller (and sometimes the buyer) is in a given country. | DE-R-*, NL-R-*, DK-R-*, SE-R-*, NO-R-*, IT-R-*, GR-R-*, IS-R-* |
The Peppol rules live in one file, PEPPOL-EN16931-UBL.sch; the EN 16931 rules in CEN-EN16931-UBL.sch. Both are on the downloads list of the
Peppol BIS Billing 3.0 specification. Belgium has no block of its own in the file: Belgian invoices are checked by the common rules, for example the enterprise number check.
Each rule has a flag. fatal means the document is invalid and an access point won't send it. warning means it can still go through, but something looks wrong, such as an Italian IPA code in the wrong format.
The two identifiers that decide everything
Every Peppol BIS Billing 3.0 invoice starts with a specification identifier (BT-24) and a business process (BT-23):
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
The CustomizationID tells a validator which rules to apply. A standard Peppol invoice carries exactly the value above; an XRechnung, for instance, carries a different one and gets the German rules instead. Leave out the ProfileID and you get two fatal errors at once (the 3.0.21 rule texts, as printed by the script below):
fatal PEPPOL-EN16931-R001 [PEPPOL-EN16931-R001]-Business process
MUST be provided.
fatal PEPPOL-EN16931-R007 [PEPPOL-EN16931-R007]-Business process
MUST have an approved identifier.
Option 1: check it online
Our free validator reads the specification identifier and picks the rules from it. For a Peppol BIS Billing 3.0 invoice or credit note it runs the UBL 2.1 schema, the EN 16931 rules (1.3.16) and the OpenPeppol rules of Peppol BIS Billing 3.0.21, the release that is mandatory now. To use it:
- Open the validator, choose the XML file (up to 10 MB) or paste the XML.
- Click Check invoice. You get a "Valid" or "Not valid" verdict, the rule sets and versions that ran, and each error and warning with its rule ID, the rule text and where in the document it applies.
- Below the verdict, the invoice is laid out like a paper invoice, so you can see what the XML actually says.
If the identifier isn't one it recognises, it says so and checks the file against the EN 16931 core rules only. The file is checked in memory and never stored.
Passing means the file meets the published rules. It doesn't guarantee that the receiver's software accepts it, or that the receiver is registered on Peppol for this document type.
Option 2: run the official rules yourself
Run the checks yourself when you generate invoices in bulk or want them in your build. The Schematron files have to be compiled to XSLT and run with an XSLT 2.0 processor.
This Python script uses Saxon-HE (pip install saxonche) and the SchXslt compiler, and expects the two .sch files from the
BIS downloads next to it:
# curl -LO https://repo1.maven.org/maven2/name/dmaus/schxslt/schxslt/1.10.1/schxslt-1.10.1.jar
# curl -LO https://docs.peppol.eu/poacc/billing/3.0/files/CEN-EN16931-UBL.sch
# curl -LO https://docs.peppol.eu/poacc/billing/3.0/files/PEPPOL-EN16931-UBL.sch
import sys, tempfile, zipfile
import xml.etree.ElementTree as ET
from saxonche import PySaxonProcessor
SVRL = "{http://purl.oclc.org/dsdl/svrl}"
failed = False
with tempfile.TemporaryDirectory() as tmp, PySaxonProcessor(license=False) as proc:
zipfile.ZipFile("schxslt-1.10.1.jar").extractall(tmp)
xslt = proc.new_xslt30_processor()
compiler = xslt.compile_stylesheet(
stylesheet_file=f"{tmp}/xslt/2.0/pipeline-for-svrl.xsl")
for sch in ("CEN-EN16931-UBL.sch", "PEPPOL-EN16931-UBL.sch"):
rules = xslt.compile_stylesheet(
stylesheet_text=compiler.transform_to_string(source_file=sch))
report = rules.transform_to_string(source_file=sys.argv[1])
for fa in ET.fromstring(report).iter(SVRL + "failed-assert"):
flag = fa.get("flag", "fatal")
failed |= flag == "fatal"
print(flag, fa.get("id"), fa.findtext(SVRL + "text").strip())
sys.exit(1 if failed else 0)
Run it as python peppol_check.py invoice.xml. It prints one line per failed rule and exits with status 1 if any of them is fatal, so a CI job fails on errors and passes with warnings.
Compiling the rules takes a few seconds; for many files, compile once and loop over the invoices. The script checks the Schematron rules only: validate against the UBL 2.1 schema as well (for example with lxml), because the rules assume a schema-valid document.
Errors you're likely to see
These are real outputs for a small fictional Belgian invoice, each with one thing broken:
| Rule | What's wrong | Fix |
|---|---|---|
PEPPOL-EN16931-R003 | Neither a buyer reference (BT-10) nor a purchase order reference (BT-13). | Add cbc:BuyerReference or cac:OrderReference/cbc:ID. If the buyer gave you neither, ask them what to put. |
PEPPOL-EN16931-R010 | No buyer electronic address (BT-49). | Add cbc:EndpointID with a schemeID to the buyer party: their Peppol ID. |
PEPPOL-EN16931-R020 | No seller electronic address (BT-34). | The same for your own party. |
PEPPOL-COMMON-R043 | A Belgian enterprise number (scheme 0208) that isn't 10 digits or fails its check digits. | Use the number exactly as registered, without dots or the BE prefix. |
PEPPOL-EN16931-R008 | An empty element somewhere in the document. | Leave optional elements out instead of writing them empty. |
The enterprise number error looks like this in the XML. The last two digits of a Belgian enterprise number are a check on the first eight (97 minus the remainder after dividing by 97), so a made-up or mistyped number fails:
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0987654321</cbc:EndpointID>
…
fatal PEPPOL-COMMON-R043 [PEPPOL-COMMON-R043]-Belgian enterprise
number MUST be stated in the correct format.
Invoices that come from Stripe often hit R003 and R010: Stripe has no field for a buyer reference or a Peppol ID, so they have to come from somewhere else, such as customer metadata. Our Stripe to EN 16931 field map shows where each value can come from.
What validation doesn't tell you
- Whether the receiver can get it. The rules check that an electronic address is there and well-formed, not that it's registered on Peppol. Delivery needs the receiver's ID in the Peppol directory (SMP) for this document type.
- Whether the content is right. A VAT number in the right format can still be the wrong one, and a valid VAT category can still be the wrong treatment.
- The envelope. Access points wrap each document in a business message envelope (SBDH) before sending. That is the access point's job; OpenPeppol publishes separate envelope rules for it.
OpenPeppol requires sending access points to validate outgoing documents against the Peppol BIS before sending (Peppol Interoperability Framework, peppol.org). Validating first means you see the errors before your access point rejects the file.
Which version to validate against
Peppol BIS Billing 3.0 gets two releases a year, each with a date from which it is mandatory. Version 3.0.20 has been mandatory since 23 February 2026, and version 3.0.21, published on 20 May 2026, since 17 August 2026. 3.0.21 changed the identifier check (R004), turned two Danish identifier rules from warnings into errors, added format checks for more identifier schemes, and removed unusable codes from the electronic address scheme list (Peppol BIS Billing 3.0 release notes). Access points validate against the mandatory release, so pin your rules to it and update when the next one is announced. Our validator runs 3.0.21 and lists the rule versions it used with each result.
Related guides
- Belgium's Peppol mandate for Stripe users: who has to send Peppol invoices, and how to get them out of Stripe.
- Mapping a Stripe invoice to EN 16931: the field-by-field map behind every Peppol invoice.
- Stripe credit notes as e-invoices: what changes for a UBL CreditNote.
- How to validate an XRechnung: the German counterpart, with the KoSIT validator.
Where evoicely fits
Check a Peppol invoice now, free and without signing up. evoicely sends Stripe invoices and credit notes over Peppol for Belgian businesses: each one is converted to Peppol BIS Billing 3.0, checked against these official rules, and sent through Recommand, a Belgian Peppol access point. Connect your Stripe account and switch on Peppol in your settings; it's free for 10 e-invoices a month.
This is general information, not legal or tax advice. A file that passes validation is technically valid against the published rules; check what applies to your business with your tax advisor.