For developers · GitHub Action and CLI

On GitHub now; Marketplace listing coming soon

Peppol 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. 1

    Add the workflow

    Save this as .github/workflows/peppol-ubl-check.yml and point files at your invoices or test fixtures. Without it, every *.xml in the repository is checked; XML files that aren't invoices, such as a pom.xml, are skipped.

    Save as .github/workflows/peppol-ubl-check.yml
    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: kalmwerk/peppol-ubl-check@v1
            with:
              files: "invoices/**/*.xml"
    
  2. 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:

sh
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:

Save as .github/workflows/peppol-ubl-check.yml
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

RuleLineWhat's wrongHow to fix
BR-CO-1488VAT 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-1791VAT 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-R120140Line 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.

InputDefaultWhat it does
files**/*.xmlInvoice 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/**.
profilepeppolpeppol: Peppol and EN 16931 rules on every UBL file. auto: pick per file from its CustomizationID. en16931 or xrechnung: always those rules.
fail-onerrorerror fails the job on errors, warning also on warnings, never only reports.
non-invoiceskipXML that isn't an invoice: skip lists it as skipped, fail counts it as an error.
if-no-files-founderrorNo invoice found: error fails the job, warn only warns, ignore says nothing.
json-file–Also write the full report as JSON to this path.
summarytrueWrite the results to the job summary.
annotationstrueAnnotate each finding on its file and line (up to 50).

Outputs

For later steps in the workflow:

result
passed or failed
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:

sh
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?
Yes. It's MIT-licensed: use it in private and commercial repositories, change it, ship it. There's no paid version of the Action and no usage limit, because nothing runs on our side.
Does it send my invoices anywhere?
No. The rule engine is bundled into the Action and runs in your runner. Invoices often hold customer names, addresses and bank details; they stay where they are.
How is it different from the online validator?
Same rules, same fixes. The website adds a visual report of one invoice (header, parties, lines, VAT breakdown, with the failing lines highlighted) and reads Factur-X / ZUGFeRD PDFs. The Action is for checking many files automatically, on every change.
Does a pass mean Peppol will accept the invoice?
It's a strong signal, not a guarantee. The 153 rules cover the calculation, VAT and identifier errors that most often get invoices rejected, but they're a partial version of the official Schematron rules, without XSD schema validation. Your access point validates every invoice against the full set before sending it.
Where is it on GitHub?
At github.com/kalmwerk/peppol-ubl-check, with its releases and changelog. Use 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?
Yes. 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?
UBL 2.1 invoices and credit notes (Peppol BIS Billing 3.0, XRechnung UBL, plain EN 16931) and UN/CEFACT CII XML. XML files that aren't invoices are skipped, unless you set non-invoice: fail.