← Back to all articles

Common Peppol BIS 3.0 validation errors and how to fix them

SealDoc Team · · 6 min read

Peppol BIS 3.0 validation failures fall into a predictable set of categories. The error messages reference business rule codes (BR-xx) and business term numbers (BT-xx) from the EN16931 standard, which requires reading the spec to understand. This article maps the most common failures to their causes and provides concrete fixes.

All examples use UBL 2.1 syntax. For CII equivalents, see UBL vs CII.

BR-01 through BR-06: missing mandatory header fields

These rules enforce the presence of top-level mandatory fields. They fail when an element is absent or empty.

RuleFieldBTFix
BR-01Specification identifierBT-24Add CustomizationID with the exact Peppol BIS 3.0 URI
BR-02Invoice numberBT-1Add non-empty ID element
BR-03Invoice issue dateBT-2Add IssueDate in YYYY-MM-DD format
BR-04Invoice currency codeBT-5Add DocumentCurrencyCode with ISO 4217 code
BR-05Seller nameBT-27Add AccountingSupplierParty/Party/PartyName/Name
BR-06Buyer nameBT-44Add AccountingCustomerParty/Party/PartyName/Name

BR-01 specific: the most common cause is using the EN16931 base URI instead of the Peppol BIS 3.0 URI. These look similar but are different:

Wrong (EN16931 base):

<cbc:CustomizationID>urn:cen.eu:en16931:2017</cbc:CustomizationID>

Correct (Peppol BIS 3.0):

<cbc:CustomizationID>
  urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1
</cbc:CustomizationID>

The validator checks the exact string. See Peppol BIS 3.0 introduction for context on why this matters.

BR-CO-10: sum of line amounts does not match total

[BR-CO-10]-Sum of Invoice line net amount (BT-106) = 
sum of Invoice line net amount (BT-131).

This rule requires that LegalMonetaryTotal/LineExtensionAmount (BT-106) equals the sum of all InvoiceLine/LineExtensionAmount values (BT-131).

The failure is almost always a rounding error. If you round each line amount independently before summing, the sum of rounded values may differ from the rounded sum of raw values.

Wrong (rounding per line, then summing):

// Line 1: 3 × 33.333... = 100.00 (rounded)
// Line 2: 3 × 33.333... = 100.00 (rounded)
// Sum of rounded: 200.00
// But raw sum: 3 × 33.333... + 3 × 33.333... = 200.00 exactly in this case

The failure is more visible with rates like 7% VAT on varying quantities where the rounding accumulates across many lines.

Correct approach: sum the raw unrounded values across all lines, then round the aggregate once:

var lineNetTotal = invoiceLines.Sum(l => l.Quantity * l.UnitPrice);  // raw
var lineNetTotalRounded = Math.Round(lineNetTotal, 2, MidpointRounding.AwayFromZero);

BR-CO-15: invoice VAT amount arithmetic failure

[BR-CO-15]-Invoice total VAT amount (BT-110) = 
sum of VAT category tax amount (BT-117).

The total VAT amount in TaxTotal/TaxAmount must equal the sum of TaxTotal/TaxSubtotal/TaxAmount values across all tax categories.

For a single-rate invoice this seems trivial, but failures occur when:

  • There are multiple VAT rates and the rounding is applied per category rather than to the aggregate
  • A zero-rate or exempt category is present and its TaxAmount is 0.00 rather than being absent
  • The TaxAmount at the TaxTotal level is computed independently from the TaxSubtotal amounts

Fix: compute all TaxSubtotal/TaxAmount values first, sum them, and use that sum as the TaxTotal/TaxAmount value. Do not compute TaxTotal/TaxAmount separately.

BR-CO-16: amount due for payment

[BR-CO-16]-Amount due for payment (BT-115) = 
Invoice total with VAT (BT-112) - Paid amount (BT-113) + Rounding amount (BT-114).

If BT-113 (paid amount) and BT-114 (rounding amount) are not present, this simplifies to BT-115 = BT-112. The failure occurs when PayableAmount and TaxInclusiveAmount differ by more than the allowed rounding tolerance.

Common cause: computing PayableAmount from rounded line amounts and TaxInclusiveAmount from a separate total calculation that produces a slightly different result.

Fix: compute one canonical grand total, use it for both TaxInclusiveAmount and PayableAmount.

BR-S-08 and BR-S-09: VAT rate and amount consistency

[BR-S-08]-For each different value of VAT category rate (BT-119) where the 
VAT category code (BT-118) is "S", the VAT category taxable amount (BT-116) 
in a VAT breakdown (BG-23) shall equal the sum of Invoice line net amounts 
(BT-131) where the VAT category code of the Invoice line (BT-151) is "S" 
and the VAT rate for the Invoice line (BT-152) equals the VAT category rate.

This rule groups all lines by VAT rate and verifies that the taxable amount in each TaxSubtotal matches the sum of lines with that rate. It fails when:

  • A line has a VAT rate that does not have a corresponding TaxSubtotal
  • A TaxSubtotal exists for a rate but no lines use that rate
  • Rounding differences accumulate between line-level and subtotal-level calculations

Fix: build the TaxSubtotal elements dynamically from the line items, grouping by (VAT category code, VAT rate), rather than constructing them independently.

BR-E-01 / BR-AE-01: reverse charge and tax-exempt invoices

[BR-E-01]-An Invoice that contains an Invoice line (BG-25), a Document level 
allowance (BG-20) or a Document level charge (BG-21) where the VAT category 
code (BT-151) is "E" shall contain exactly one VAT breakdown...

Invoices with exempt (E), reverse charge (AE), or zero-rate (Z) VAT require specific TaxCategory structures that differ from standard (S) rate invoices.

Common mistakes:

  • Using ID value S for all lines regardless of the actual VAT treatment
  • Omitting the TaxExemptionReasonCode or TaxExemptionReason elements required for exempt categories
  • Missing AccountingSupplierParty/Party/PartyTaxScheme when the invoice uses reverse charge

For reverse charge (AE), the VAT identifier of the buyer’s tax scheme must be present, and the seller’s VAT identifier must also be present.

Peppol-specific extension rules: Germany

German Peppol profiles add rules on top of EN16931 base. Common failures:

BR-DE-TMP-32: delivery date (BT-72) is required for German B2G invoices. Add Delivery/ActualDeliveryDate in YYYY-MM-DD format.

BR-DE-18: either the seller’s VAT identifier (BT-31) or a tax registration number (BT-32) must be present. Missing both causes rejection. If the seller is VAT-exempt, provide BT-32 with the tax registration number instead.

BR-DE-16: the buyer reference (BT-10, Leitweg-ID) is mandatory for German B2G invoices. The Leitweg-ID format is validated: it must match the pattern \d{2,12}-\d{4,12}-\d{2}.

Debugging efficiently

The Schematron error output includes an XPath location:

<svrl:failed-assert location="/Invoice[1]/cac:LegalMonetaryTotal[1]">
  <svrl:text>[BR-CO-10]-Sum of Invoice line net amounts...</svrl:text>
</svrl:failed-assert>

Use the XPath location to navigate directly to the failing element. For arithmetic rules, add debug output that logs the raw values before rounding so you can trace where the discrepancy originates.

The pre-compiled XSLT files from the Peppol BIS GitHub releases are the authoritative validation artifacts. Run them with Saxon HE as described in generating Peppol BIS 3.0 invoices in C#. Running against the latest release is important: rule sets are updated with each Peppol BIS release, and a rule that passes against an older XSLT may fail against the current one.

SealDoc and validation errors

SealDoc runs EN16931 and Peppol BIS 3.0 Schematron validation on every invoice before delivery. When validation fails, the API returns a structured error response mapping each failure to its BR code, the violated BT, and the XPath location in the submitted document.

For organizations debugging an existing invoice generation pipeline, the SealDoc public validator accepts UBL and CII invoice files, runs the current Peppol BIS 3.0 XSLT, and returns the full list of failures with plain-language descriptions. Rule sets are kept current with Peppol BIS releases, so the validator always reflects the rules that a receiving Access Point will apply.


← Back to all articles