The short answer
BR-48 fails for each cac:TaxSubtotal whose VAT tax category has no cbc:Percent, unless the category is O. Add cbc:Percent to the breakdown cac:TaxCategory, straight after cbc:ID, with the rate as a percentage: 20 for 20%, and 0 for zero rated, exempt and the other categories that charge no VAT.
Category O is the single exception. A supply outside the scope of VAT has no rate to state, so its breakdown may omit the element.
What the rule checks
The rule runs on every cac:TaxSubtotal inside a cac:TaxTotal and reads the cac:TaxCategory whose tax scheme is VAT. It passes when that category has a cbc:Percent element, or when its cbc:ID is O.
Zero is a rate like any other. An exempt breakdown without cbc:Percent was reported by this rule alone when tried; the fix there is 0, not leaving the element out.
The exception only permits the rate to be absent. A category O breakdown carrying a cbc:Percent of 0 was also accepted when tried.
An empty cbc:Percent is not a number. When tried it failed the XSD layer, so this rule and the other business rules were never evaluated.
Line rates are not read. With the rate removed from a Standard rated line instead, the findings were BR-S-05 and BR-S-08 when tried, not this rule.
| Term | Meaning | UBL element |
|---|---|---|
| BT-119 | VAT category rate | cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:Percent |
| BT-118 | VAT category code | cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cbc:ID |
How an integration ends up here
Possible causes, from the shape of the rule rather than from measured usage:
- Zero rates are treated as no rate and suppressed, so zero rated and exempt breakdowns lose
cbc:Percent. - The VAT summary is grouped by category only and the rate is not carried into the breakdown.
- The exception for
Ois applied to every category that charges no VAT, removing the rate from exempt or reverse-charge breakdowns too. - The rate is held as text such as
Standard, and the export drops it when it cannot be written as a number.
How to fix it
- For each breakdown, take the rate of the lines, allowances and charges it covers. A breakdown groups items by category and rate, so there is exactly one.
- Write it to
cbc:Percentinside the breakdowncac:TaxCategory, aftercbc:IDand before any exemption reason, as a percentage number:20, not0.2or20%. - For
Z,E,AE,KandG, write0. Leave the element out only forO. - Recalculate the subtotal
cbc:TaxAmountfrom the taxable amount and the rate, sinceBR-CO-17checks the two together.
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 Standard rated breakdown gives no rate
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="GBP">25.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="GBP">5.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>Fragment of the corrected invoice: the rate of 20 follows the category code
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="GBP">25.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="GBP">5.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>20</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>The corrected invoice has <cbc:Percent>20</cbc:Percent> after the category code in the breakdown; nothing else differs. The failing document also reports BR-CO-17 and BR-S-09, which both check the 5.00 of tax as taxable amount x rate / 100 and cannot reach 5.00 without a rate. BR-CO-17 handles a missing rate like a zero one and expects tax that rounds to nothing; BR-S-09 has no answer at all without a rate. Restoring the rate clears all three.
What the validator reported
- The failing invoice reports BR-48, BR-CO-17 and BR-S-09. 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 UBL
InvoiceandCreditNote. A credit note with the rate removed from its breakdown reported the same three rules when tried. - Reported by the EN 16931 layer; the Peppol layer passes the recorded example.
- A breakdown whose tax scheme is not
VAThas no VAT rate as far as this rule can see: when tried, one underGSTfailed here despite itscbc:Percent, alongsideBR-47andBR-CO-17.
Related rules
- BR-CO-17 checks the breakdown tax amount against the rate and is reported with this rule
- BR-S-09 is the Standard rated tax calculation that also needs the rate
- BR-47 requires the category code that decides whether the rate may be left out
- BR-O-05 goes the other way for lines: a line in category O must not carry a rate
- 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-48 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.
