Validation
Two questions, two kinds of tools: is the invoice a valid EN 16931 invoice in its syntax and profile, and does it follow VINvoice?
How the corpus was validated
Every test invoice was checked with the official validation artefacts of each format it claims: the XML schemas, the CEN EN 16931 schematron and the schematron of the national or network profile. XSD validation ran on a Java 21 runtime (Eclipse Temurin); the schematron rules ran as compiled XSLT with Saxon-HE 12.5. The artefacts were downloaded from their publishers and used unchanged. They are not redistributed here.
| Validator | Files | Rules fired | Failed |
|---|---|---|---|
| Factur-X 1.09.2 EN 16931 XSD | 13 | – | 0 |
| UN/CEFACT CII D16B XSD | 14 | – | 0 |
| Factur-X 1.09.2 EXTENDED XSD | 1 | – | 0 |
| UBL 2.1 Invoice / CreditNote XSD | 5 | – | 0 |
| CEN EN 16931 validation 1.3.17 | 19 | 2099 | 0 |
| Factur-X 1.09.2 EN 16931 schematron | 12 | 1514 | 0 |
| Factur-X 1.09.2 EXTENDED schematron | 1 | 138 | 0 |
| KoSIT XRechnung 3.0.2 schematron 2.6.0 | 2 | 39 | 0 |
| Peppol BIS Billing 3.0.20 | 4 | 172 | 0 |
“Failed” counts schema errors (XSD) and failed assertions plus reports (schematron), warnings included. “Rules fired” is the number of schematron rule contexts that matched, summed over all files.
Results per file
| File | Factur-X XSD | CII D16B XSD | Factur-X EXT XSD | UBL 2.1 XSD | CEN | Factur-X | Factur-X EXT | XRechnung | Peppol | VINvoice checker |
|---|---|---|---|---|---|---|---|---|---|---|
credit-25a.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
credit-25a.peppol.ubl.xml
|
– | – | – | 0 | 0 | – | – | – | 0 | level 2 |
export-25a.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
level0.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 0 · 1 info |
new-intra-eu-unregistered.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
new-intra-eu.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
sale-25a-level1.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 1 |
sale-25a-xborder.peppol.ubl.xml
|
– | – | – | 0 | 0 | – | – | – | 0 | level 2 |
sale-25a.ext.cii.xml
|
– | 0 | 0 | – | 0 | – | 0 | – | – | level 2 |
sale-25a.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
sale-25a.peppol.ubl.xml
|
– | – | – | 0 | 0 | – | – | – | 0 | level 2 |
sale-25a.xr.cii.xml
|
0 | 0 | – | – | 0 | – | – | 0 | – | level 2 |
sale-25a.xr.ubl.xml
|
– | – | – | 0 | 0 | – | – | 0 | – | level 2 |
sale-regular-odometer-inoperative.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 |
workshop.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 · 2 info |
workshop.peppol.ubl.xml
|
– | – | – | 0 | 0 | – | – | – | 0 | level 2 · 2 info |
neg-margin-k.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 · 4 errors · 2 warnings |
neg-profile.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 2 · 3 errors · 4 warnings |
neg-profile2.fx.cii.xml
|
0 | 0 | – | – | 0 | 0 | – | – | – | level 1, 2 · 4 errors · 4 warnings · 4 info |
Are the validators live?
A run with zero findings proves little unless the same setup also rejects broken input.
Deliberately broken copies of the test invoices were rejected as expected: the scheme
VIN instead of AKG fails the CEN rule BR-CL-07; two line object identifiers
on one line fail the XSD and the Peppol rule R100; two invoice notes on a cross-border Peppol invoice
fail the Peppol rule R002.
Not yet run
The KoSIT validator application, the official FNFE and FeRD validators, Mustangproject, Peppol BIS 3.0.21, delivery through Peppol access points, and import into accounting and procurement systems. Results will be added here as they come in.
Reference checker
The format validators above do not know VINvoice. The reference checker reads an invoice (CII or UBL), applies the reading rules and reports where a vehicle line departs from the convention. It takes grammars, labels and the statutory flag from registry.json, so it follows the registry without code changes. It never rejects an invoice: an error means only that the line does not conform to VINvoice. The checker is a single Python file without dependencies, licensed under Apache-2.0 (checker/vehicle_check.py) . A regression test runs it on every test invoice and compares the result with the expected findings.
Schematron
The same rules are published as ISO Schematron, vinvoice.sch, generated from the registry. It runs on CII and UBL invoices in any XSLT 2.0 Schematron processor, next to the EN 16931 and national rules. Rule identifiers match the checker. The VIN check digit (V-05), the six-months-or-6,000-km test of V-08, the registration-number comparison (V-18) and the reader notes on level 0 and attached lines (V-19, V-20) stay with the checker. On the test corpus both report the same errors and warnings; continuous integration checks this on every change.
| Rule | Severity | Finding |
|---|---|---|
| V-01 | error | A marker vehicle.specificationId without a role row vehicle.roleCode. |
| V-02 | error | A value does not match the grammar of its key and is dropped, or the marker value is malformed and the line is treated as unmarked. |
| V-03 | warning | A key that is not repeatable appears twice on a line; the first value is kept. |
| V-04 | warning | BT-128 and the row vehicle.vin differ; BT-128 wins. |
| V-05 | warning | The VIN is not 17 characters long or its check digit fails. Older and non-EU vehicles can legitimately deviate, so this is never an error. |
| V-06 | error | A supply or trade-in line with a quantity other than 1. |
| V-07 | error, warning |
vehicle.marginSchemeIndicator on a line with VAT category K (error); with a category
other than E, F or G, without the margin-scheme
note in BG-1, or together with condition new (warnings).
|
| V-08 | error, warning |
vehicle.newMeansOfTransportIndicator without vehicle.maximumNetPowerKw or vehicle.engineCapacityCm3, or without any newness feature (errors); without an odometer reading, registered but
without vehicle.firstEntryIntoServiceDate, or beyond both 6,000 km and six months
(warnings).
|
| V-09 | warning, info |
A used vehicle supplied or traded in without an odometer reading or a status saying none
is available (warning; only where vehicle.categoryCode is empty or an M, N or L category);
without vehicle.firstRegistrationDate (info: correct only if it was never registered).
|
| V-10 | warning | A service line without VIN or registration number, or without an odometer reading or status (for vehicles that have an odometer). |
| V-11 | error, info | A statutory key written without its label (error); a key longer than 40 or a name longer than 64 characters, a label that is not in the registry, or a marker that is not the first row (info). |
| V-12 | error | More than one BT-128 on a line. |
| V-13 | error | vehicle.odometerReadingKm and vehicle.odometerReadingMi on the same line. |
| V-14 | error | A row uses the measure or quantity variant of BT-161 instead of its text value. |
| V-15 | info | Rows on a line without a marker are ignored. |
| V-16 | warning | vehicle.batteryStateOfHealthPercent without vehicle.batteryStateOfHealthDate or without vehicle.batteryStateOfHealthMethodCode. |
| V-17 | error |
An odometer reading although vehicle.odometerReadingStatusCode says the reading is not available.
|
| V-18 | warning | A registration number in BT-128 (scheme ABZ) differs from vehicle.registrationNumber. |
| V-19 | info | Level 0: a vehicle identified only by BT-128 or BT-18. |
| V-20 | info | A line without a marker is attached to a vehicle through its BT-128. |
| V-21 | warning | Unknown major version in the marker; the rows of that line are skipped. |
| V-22 | info |
Unknown key or reserved name, ignored. Vendor rows
(reverse-DNS names outside vehicle.) are ignored without a finding.
|
Checker output for the negative controls
neg-margin-k.fx.cii.xml
(6 findings)
Negative control for the checker: margin-scheme flag on an intra-EU exempt line (category K) and further faults. Valid EN 16931, not a valid VINvoice line.
| Rule | Severity | Line | Message |
|---|---|---|---|
| V-06 | error | 1 | role supply with quantity 2; one vehicle per line |
| V-07 | error | 1 | margin scheme on an intra-EU supply (category K): excluded by Art. 139(3) and Art. 313(2) Directive 2006/112/EC |
| V-07 | warning | 1 | margin line but no BG-1 note with the Art. 226(14) mention (for example 'Margin scheme - Second-hand goods' or 'Gebrauchtgegenstände/Sonderregelung') |
| V-08 | error | 1 | new means of transport without vehicle.maximumNetPowerKw or vehicle.engineCapacityCm3 |
| V-08 | error | 1 | new means of transport without any newness feature (no odometer reading, no vehicle.firstEntryIntoServiceDate) |
| V-09 | warning | 1 | used supply without an odometer reading (or status inoperative/unknown) |
neg-profile.fx.cii.xml
(7 findings)
Negative control for the checker: malformed rows (statutory key without its label, duplicates, grammar violations).
| Rule | Severity | Line | Message |
|---|---|---|---|
| V-11 | error | 1 | statutory key vehicle.odometerReadingKm in bare form; the labelled form is mandatory |
| V-03 | warning | 1 | duplicate vehicle.make; first value kept |
| V-02 | error | 1 | vehicle.odometerReadingKm='48.200 km' violates the registry grammar; value dropped by the reader |
| V-02 | error | 1 | vehicle.configuration='=HYPERLINK("x")' violates the registry grammar; value dropped by the reader |
| V-04 | warning | 1 | BT-128 TSTVEH016N0000001 differs from vehicle.vin TSTVEH016N0000009; BT-128 wins |
| V-16 | warning | 1 | vehicle.batteryStateOfHealthPercent without vehicle.batteryStateOfHealthDate |
| V-16 | warning | 1 | vehicle.batteryStateOfHealthPercent without vehicle.batteryStateOfHealthMethodCode |
neg-profile2.fx.cii.xml
(12 findings)
Negative control for the checker: reader edge cases (unknown major version, unknown key, both odometer units, unmarked rows, a vendor row).
| Rule | Severity | Line | Message |
|---|---|---|---|
| V-02 | error | 1 | vehicle.firstRegistrationDate='2022-02-30' violates the registry grammar; value dropped by the reader |
| V-05 | warning | 1 | VIN TSTVEH016N0000002 is not 17 ISO 3779 characters or its check digit fails |
| V-13 | error | 1 | vehicle.odometerReadingKm and vehicle.odometerReadingMi on the same line |
| V-17 | error | 1 | odometer reading given although vehicle.odometerReadingStatusCode = inoperative |
| V-09 | info | 1 | used supply without vehicle.firstRegistrationDate (correct only if the vehicle was never registered) |
| V-18 | warning | 2 | BT-128 ABZ registration number differs from vehicle.registrationNumber |
| V-10 | warning | 2 | service line without odometer reading or status |
| V-15 | info | 3 | vehicle rows on an unmarked line are ignored |
| V-21 | warning | 4 | unknown major version vehicle/2.0; vehicle rows skipped |
| V-11 | info | 5 | label 'Hersteller' not in the registry set for vehicle.make |
| V-22 | info | 5 | unknown key vehicle.foo ignored |
| V-01 | error | 5 | marker without vehicle.roleCode |