The short answer
BR-CO-21 fails when a cac:AllowanceCharge directly under the document root, with cbc:ChargeIndicator of false, has neither cbc:AllowanceChargeReasonCode nor cbc:AllowanceChargeReason. Add the reason the allowance was granted, taken from the discount record in your source system: a code from the UNTDID 5189 allowance list, free text, or both.
BR-33 has the same test and is reported on the same allowance, so one missing reason produces two findings. Adding either element clears both.
What the rule checks
The rule visits each cac:AllowanceCharge that is a child of the Invoice or CreditNote root and whose indicator reads as false. It passes when that allowance has a reason code element, a reason text element, or both.
Either element on its own is enough. When tried, the corrected invoice with its allowance code removed, and again with its allowance text removed, passed every layer.
Only the existence of the element is tested. An empty cbc:AllowanceChargeReason, or one holding only spaces, satisfied this rule when tried, and the Peppol layer then reported it as PEPPOL-EN16931-R008.
Allowances on a line and charges at document level are not counted here; they have BR-CO-23 and BR-CO-22. The price discount inside cac:Price needs no reason at all.
Nothing compares the code with the text. BR-CO-05, the rule meant to check that both describe the same allowance, always passes in this release.
| Term | Meaning | UBL element |
|---|---|---|
| BG-20 | Document level allowances | cac:AllowanceCharge[cbc:ChargeIndicator = false] |
| BT-97 | Document level allowance reason | cac:AllowanceCharge/cbc:AllowanceChargeReason |
| BT-98 | Document level allowance reason code | cac:AllowanceCharge/cbc:AllowanceChargeReasonCode |
How an integration ends up here
Possible causes, from the shape of the rule rather than from measured usage:
- The discount is stored as an amount only, with no type or description, so the mapping has nothing to write in either element.
- The reason is mapped from an optional field such as a promotion name, which was blank for this document, and the serialiser skipped both elements.
- The mapping writes a code only when the discount type has a known UNTDID equivalent and never writes text, so an unmapped type ends up with neither.
- A goodwill reduction or settlement discount is added as an allowance by a later processing step that does not set a reason.
How to fix it
- Find the allowance from the finding location.
cac:AllowanceCharge[1]is the firstcac:AllowanceChargeunder the root, and the index counts charges as well as allowances. - Take the reason from the source record that created the allowance: the discount type, the promotion, the contract term or the rebate agreement.
- If that reason matches an entry in the UNTDID 5189 subset Peppol publishes, write the code in
cbc:AllowanceChargeReasonCode;95is Discount and100is Special rebate. If no entry fits, sendcbc:AllowanceChargeReasontext alone. - Keep the UBL order:
cbc:ChargeIndicator, then the code, then the text, thencbc:Amount. When tried, text placed before the code, or after the amount, failed the XSD layer and the later layers were skipped. - Do not emit an empty element as a placeholder. It swaps this finding for
PEPPOL-EN16931-R008.
Validate your corrected invoice
Before and after
These are fragments, not complete documents. The complete synthetic documents they come from are linked below.
Fragment of the failing invoice: the document allowance of 1.00 gives no reason
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:Amount currencyID="GBP">1.00</cbc:Amount>
<!-- tax category omitted from this fragment -->
</cac:AllowanceCharge>Fragment of the corrected invoice: the allowance carries reason code 95 and a reason text
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>95</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Example document discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="GBP">1.00</cbc:Amount>
<!-- tax category omitted from this fragment -->
</cac:AllowanceCharge>The corrected invoice has reason code 95 and the text Example document discount on its document allowance; the failing one has neither, and nothing else differs. Besides BR-CO-21, the failing document reports BR-33, an EN 16931 rule with exactly the same test on the same allowance, and the same edit clears it. The amount, VAT category and totals are untouched, so no arithmetic rule fires.
What the validator reported
- The failing invoice reports
BR-33and BR-CO-21. The corrected document passes every layer with no findings.Download the failing XMLDownload the corrected XML
Recorded on phive 12.1.0 / phive-rules-peppol 4.5.6 / Saxon-HE 12.10, the engine behind the free validator, using synthetic data. A recorded result is regression evidence for these documents; it is not a certification.
Where it applies
- Applies to
InvoiceandCreditNotealike. When tried on the credit note version of the recorded example, the same pair of findings,BR-33andBR-CO-21, was reported. - The EN 16931 rules read
cbc:ChargeIndicatoras a boolean, so an allowance written with0is still in scope: when tried without a reason, it reported this rule andBR-33, plusPEPPOL-EN16931-R043for the spelling. - A code that is present but not on the allowance list passes this rule and is rejected by
BR-CL-19.
Related rules
- BR-CO-22 asks the same of document-level charges
- BR-CO-23 requires a reason on allowances attached to a line
- BR-CL-19 checks the reason code, once present, against the UNTDID 5189 allowance list
- BR-32 requires a VAT category on the same document-level allowance
- Browse every rule in the reference
- Background: Understanding EN 16931 validation errors
Scope and source
Written for Peppol BIS Billing 3.0.21 (May 2026), EN 16931 1.3.16, as applied to UBL 2.1 Invoice and CreditNote documents. Other profiles, syntaxes and releases can define this identifier differently. Guidance version 2026-09-24.1: source checked 2026-09-24, explanation last updated 2026-09-24.
The official definition of BR-CO-21 carries the normative wording and test. This page is our explanation of it, not a copy.
Guidance does not change the engine verdict. Fixing this finding does not mean the document passes every layer, and validation does not certify legal or tax compliance or transmit a document over Peppol.
