The Peppol validation pipeline

How the Peppol Validator turns a UBL or CII XML file into a structured pass/fail report. Six stages, two pre-compiled schematron stylesheets, one XSLT 2.0 engine. The diagram below mirrors the real implementation in src/lib/validator.ts. Free to embed.

By Peppol Validator · Last updated

Peppol validation pipelineDiagram of the Peppol Validator pipeline. The XML invoice is parsed, the format is detected (UBL or CII), and the document is run through the pre-compiled CEN EN16931 schematron (SEF) and the Peppol BIS Billing 3.0 schematron (SEF) using SaxonJS XSLT 2.0. The SVRL outputs are parsed and merged into a single result with errors, warnings and infos. Source: https://peppolvalidator.com/diagrams/validation-pipelinePeppol validation pipelinePeppol Validator (peppolvalidator.com)© Peppol Validator. Free to embed with attribution link to peppolvalidator.com.https://peppolvalidator.com/diagrams/validation-pipelineValidation pipelineSTEP 1Upload XMLUBL or CII invoiceSTEP 2Detect formatUBL ↔ CrossIndustryInvoiceUBLCIISTEP 3CEN EN16931 SEFSaxonJS XSLT 2.0STEP 4Peppol BIS 3.0 SEFSaxonJS XSLT 2.0CII path: only CEN EN16931 (CII) is run.STEP 5Parse SVRL & merge issueserrors · warnings · infosSTEP 6Resultvalid · invalid · error© peppolvalidator.com
The Peppol Validator pipeline. Format detection branches between the UBL path (CEN + Peppol BIS) and the CII path (CEN only).

Stage by stage

Step 1: upload XML. The validator accepts a single XML file. Either a UBL invoice or credit note (Peppol BIS Billing 3.0) or a CII document (the format used inside Factur-X and ZUGFeRD). The file is read into memory as text.

Step 2: detect format. The validator checks for the CrossIndustryInvoice namespace to decide whether to take the UBL path or the CII path. This single check determines which schematron stylesheets are applied next.

Step 3: CEN EN16931 schematron. Whether the document is UBL or CII, it is run against the CEN EN16931 schematron. This is the European e-invoicing standard's business rule layer (the BR-* family, about 70 rules) and it applies equally to both syntaxes. The validator does not interpret schematron at runtime: it executes a pre-compiled SEF stylesheet via SaxonJS's XSLT 2.0 engine.

Step 4: Peppol BIS Billing 3.0 schematron. Only on the UBL path. The Peppol BIS profile is a CIUS on top of EN16931 maintained by OpenPeppol. It adds Peppol-specific business rules (the PEPPOL-EN16931-* family, plus a handful of country-specific rules), about 240 in total. This pass is what makes a UBL document "Peppol-conformant" rather than just "EN16931-conformant".

Step 5: parse SVRL and merge. Each schematron pass produces an SVRL document (Schematron Validation Report Language). The validator parses the SVRL with a small custom parser, classifies each fired assertion as an error, warning or info, and merges the results from both passes into a single ValidationResult.

Step 6: result. The merged result is returned to the caller. The status is "valid" if there are no errors, "invalid" if there is at least one error, or "error" if the validator itself crashed (for example because the SEF stylesheets were not loaded). Errors and warnings include the rule ID, the location in the source XML and a human-readable explanation.

Why pre-compiled SEF

Schematron is a rule language. Tools normally execute it by compiling it to XSLT first. The Peppol Validator does that compilation once, at build time, using SaxonJS's SEF (Stylesheet Export File) format. Each schematron file becomes a JSON-serialised XSLT 2.0 stylesheet that can be loaded straight into the SaxonJS engine without any further parsing.

The CEN EN16931 SEF is about 8 MB; the Peppol BIS SEF is about 2 MB. They are loaded into memory once on cold start and cached for the lifetime of the worker. The 311 schematron rules then execute as a single XSLT 2.0 transform per stylesheet, which is how the validator can return a complete result for a typical invoice in well under 100 ms.

This is also what makes the validator portable to Cloudflare Workers. There is no native dependency, no server-side schematron parser, no XSLT 1.0 fallback path. Everything is JavaScript, runs in V8 isolates and respects the Workers CPU and memory budget.

Embed this diagram

Copy the snippet below into any HTML page, blog post or wiki. The image is served as an SVG so it scales to any size and stays sharp on retina screens. Please keep the attribution line below the image.

<a href="https://peppolvalidator.com/diagrams/validation-pipeline">
  <img src="https://peppolvalidator.com/diagrams/validation-pipeline/svg"
       alt="Peppol validation pipeline diagram"
       width="800" height="480" />
</a>
<p><small>Diagram by <a href="https://peppolvalidator.com">Peppol Validator</a></small></p>

The SVG file also carries a Dublin Core copyright block (RDF metadata), so the attribution travels with the image when it is downloaded or screenshotted.

Related references

Run an invoice through the pipeline

Drop a UBL or CII XML file and see the merged EN16931 + Peppol BIS schematron output, rule by rule. Free, instant, no signup.

Frequently asked questions

What schematron rules does the Peppol Validator run?

Two sets, in sequence. The first is the EN16931 (CEN) schematron, which encodes the European e-invoicing standard's business rules and applies to every conformant invoice (about 70 BR-* rules). The second is the Peppol BIS Billing 3.0 schematron, which is the Peppol CIUS on top of EN16931 and adds Peppol-specific constraints (the PEPPOL-EN16931-* family, about 240 rules). Together that is 311 rules, all of which are checked on every UBL upload.

Why two passes?

Because the rules come from two different standards bodies: CEN for EN16931 and OpenPeppol for the Peppol BIS profile. Each maintains its own schematron file. Running them as two passes keeps the source rules unmodified, so when CEN or OpenPeppol publishes a new version, the validator can be updated by replacing the relevant SEF stylesheet without touching the other.

What is SEF and why does the validator use it?

SEF stands for Stylesheet Export File. It is a pre-compiled, JSON-encoded representation of an XSLT 2.0 stylesheet, generated by the SaxonJS toolchain. The Peppol Validator pre-compiles each schematron rule set into SEF once at build time and caches the result in memory at runtime. This avoids the cost of re-parsing the schematron on every request and lets the validator run inside a Cloudflare Worker, which has tight startup time and CPU budgets.

How does CII validation differ from UBL validation?

CII (Cross-Industry Invoice, used in Factur-X and ZUGFeRD) only goes through the CEN EN16931 schematron. The Peppol BIS Billing 3.0 schematron is UBL-only and is skipped. The format-detection step at the start of the pipeline switches between the two paths automatically based on the root namespace of the uploaded XML.

Where does the SVRL output come from?

SVRL stands for Schematron Validation Report Language. It is the standardised XML format that a schematron run produces: every fired-rule, successful-report and failed-assert is recorded with its location, severity and message. The Peppol Validator parses the merged SVRL output into a structured ValidationResult that lists errors, warnings and infos, with the rule ID, the location in the source XML and a human-readable explanation.