For developers · GitHub Action and CLI
On GitHub now; Marketplace listing coming soonPeppol UBL check: a free GitHub Action for your e-invoices
Check the UBL / Peppol BIS Billing 3.0 invoices your code generates on every push and pull request. The job fails on errors, and the summary gives you the rule ID, the line and how to fix it, before an access point rejects the invoice.
- 153 EN 16931, Peppol and XRechnung rules
- Runs offline, no API key
- MIT licence
- Same rules as the online validator
Use kalmwerk/peppol-ubl-check@v1 in any workflow, or keep a downloaded copy in your repository. Latest release on GitHub
Also in:NederlandsFrançais
Set it up in two steps
Add one workflow file. Nothing to install, no API key.
- 1
Add the workflow
Save this as
.github/workflows/peppol-ubl-check.ymland pointfilesat your invoices or test fixtures. Without it, every*.xmlin the repository is checked; XML files that aren't invoices, such as apom.xml, are skipped.Save as .github/workflows/peppol-ubl-check.ymlname: Peppol UBL check on: push: branches: [main] pull_request: permissions: contents: read jobs: invoices: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: kalmwerk/peppol-ubl-check@v1 with: files: "invoices/**/*.xml" - 2
Push
The job fails when an invoice has errors. The job summary lists each finding with its rule ID, line and fix, and pull requests show them next to the code.
Prefer a copy in your repository?
Download the zip and unzip it into .github/actions/, from your repository root:
mkdir -p .github/actions unzip peppol-ubl-check-1.0.1.zip -d .github/actions/ git add .github/actions/peppol-ubl-check
Then use the local path in the workflow:
name: Peppol UBL check
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
invoices:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/peppol-ubl-check
with:
files: "invoices/**/*.xml"
Only action.yml and dist/ run. The zip's test invoices stay inside .github/, which the default files pattern skips.
What the job summary shows
For one of our deliberately broken sample invoices (a VAT calculation error), the summary lists, among others:
❌ invoices/2026-0918.xml
| Rule | Line | What's wrong | How to fix |
|---|---|---|---|
| BR-CO-14 | 88 | VAT total does not match breakdown. VAT breakdown adds up to 403.04, but the VAT total is 422.64. | Make cac:TaxTotal/cbc:TaxAmount equal to the sum of all cac:TaxSubtotal/cbc:TaxAmount values. |
| BR-CO-17 | 91 | VAT amount does not match rate. 1960.00 × 21% = 411.60, but the VAT amount is 392.00. | Calculate VAT per category on the taxable total (not per line) and round once to 2 decimals. |
| PEPPOL-EN16931-R120 | 140 | Line net amount calculation error. Line 2: 8 × 95 = 760.00, but the line amount is 712.50. | Recalculate cbc:LineExtensionAmount = quantity × price / base quantity + charges − allowances, rounded to 2 decimals. |
…and 2 more rows, below an overview table with one row per invoice. Each rule ID links to its explanation.
What it checks
- 101 EN 16931 business rules: mandatory fields, VAT categories and exemptions, line, total and VAT breakdown calculations, code lists.
- 28 Peppol BIS Billing 3.0 rules: specification IDs, buyer reference, endpoint IDs and Belgian 0208 numbers, allowed document types.
- 13 XRechnung rules, for files that declare XRechnung (or with
profile: xrechnung). - 11 extra checks: well-formed XML with the exact line, VAT number and Belgian enterprise number check digits, IBAN, due date.
What it doesn't check
- The official Schematron rule sets in full: the 153 rules are a partial version.
- XSD schema validation (element order, unknown elements).
- PDF/A and Factur-X / ZUGFeRD PDFs: it reads XML files only.
- Whether the buyer can receive invoices on Peppol: that needs a network lookup.
- A pass is a strong signal, not a guarantee: your access point still validates every invoice.
Inputs
All optional. The defaults suit a repository with invoices or test fixtures in it.
| Input | Default | What it does |
|---|---|---|
| files | **/*.xml | Invoice files, as globs from the repository root, one per line or comma separated. A folder means every *.xml in it; !pattern leaves files out. |
| ignore | – | Globs to leave out, e.g. test/fixtures/broken/**. |
| profile | peppol | peppol: Peppol and EN 16931 rules on every UBL file. auto: pick per file from its CustomizationID. en16931 or xrechnung: always those rules. |
| fail-on | error | error fails the job on errors, warning also on warnings, never only reports. |
| non-invoice | skip | XML that isn't an invoice: skip lists it as skipped, fail counts it as an error. |
| if-no-files-found | error | No invoice found: error fails the job, warn only warns, ignore says nothing. |
| json-file | – | Also write the full report as JSON to this path. |
| summary | true | Write the results to the job summary. |
| annotations | true | Annotate each finding on its file and line (up to 50). |
Outputs
For later steps in the workflow:
- result
passedorfailed- checked
- invoices checked
- failed
- invoices with findings at or above
fail-on - errors
- errors across all invoices
- warnings
- warnings across all invoices
- skipped
- matching files that aren't invoices
- json-file
- path of the JSON report
Command line
The zip includes the same check as a command-line tool, for your machine or any other CI (GitLab, Jenkins, Azure DevOps), with Node.js 20 or newer:
node .github/actions/peppol-ubl-check/dist/cli.js "invoices/**/*.xml" node .github/actions/peppol-ubl-check/dist/cli.js --json build/invoices > peppol-report.json
Exit code 0: no errors; 1: errors; 2: a usage error or no invoices found. --help lists every option.
An npm package, so npx peppol-ubl-check works without the download, is planned.
Nothing leaves your runner. The Action makes no network requests: no account, API key, analytics or telemetry. It reads the files you point it at and writes the summary, the annotations and the optional JSON report. The only third-party code in it is the XML reader saxes.
Questions
Is it really free?
Does it send my invoices anywhere?
How is it different from the online validator?
Does a pass mean Peppol will accept the invoice?
Where is it on GitHub?
kalmwerk/peppol-ubl-check@v1, which always points at the newest 1.x release; the download stays available for repositories that only allow local actions. A GitHub Marketplace listing is coming soon.Can I use it outside GitHub?
dist/cli.js from the zip runs anywhere with Node.js 20 or newer, exits non-zero on errors and can write the same JSON report, so it fits GitLab CI, Jenkins, Azure DevOps or a pre-commit hook.Which files does it check?
non-invoice: fail.