Every e-invoicing integration eventually produces an invoice that is wrong by exactly one cent. The amounts look fine in your database, your unit tests pass, the PDF renders beautifully — and the validator rejects the XML with something like BR-CO-15: Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110). You diff the numbers and find 119.00 != 118.99.
This is not a validator being pedantic. It's a mismatch between how your code rounds and how EN 16931 expects you to round — and the standard is more specific about this than most teams assume. This article walks the whole arithmetic: the calculation chain, the decimal limits, where rounding is mandatory, where it's forbidden, and the one architectural decision (per-line VAT) that causes most one-cent bugs.
The calculation chain the validator replays
EN 16931 doesn't treat your totals as data; it treats them as claims it can re-derive. The BR-CO ("calculation") rules chain the monetary business terms together, bottom to top:
- BR-CO-10 — Sum of invoice line net amounts (BT-106) = Σ line net amount (BT-131) over all lines.
- BR-CO-13 — Invoice total without VAT (BT-109) = BT-106 − document-level allowances (BT-107) + document-level charges (BT-108).
- BR-CO-17 — For each VAT category: category tax amount (BT-117) = category taxable amount (BT-116) × rate (BT-119) / 100, rounded to two decimals. And BR-CO-14: total VAT (BT-110) = Σ BT-117.
- BR-CO-15 — Total with VAT (BT-112) = BT-109 + BT-110.
- BR-CO-16 — Amount due (BT-115) = BT-112 − prepaid amount (BT-113) + rounding amount (BT-114).
Two things follow immediately. First, you cannot compute any of these totals independently — each is defined in terms of the previous one, so a rounding decision at the bottom propagates all the way to the amount due. Second, only one step in the chain says "rounded to two decimals": the VAT computation in BR-CO-17. The Peppol BIS 3.0 spec makes the general principle explicit: line net amounts and document-level amounts are rounded to two decimals, but "results from calculations involving already rounded amounts are not subject to rounding" — the additions in BR-CO-13, BR-CO-15 and BR-CO-16 are exact sums of already-rounded inputs. Round once, at the point where a multiplication happens; never re-round a sum.
Decimals: where two is the law and where it isn't
The BR-DEC rule family caps every amount-typed field — line net amounts, allowance and charge amounts, all document totals — at two decimals. BR-DEC-14, for instance: "The allowed maximum number of decimals for the Invoice total amount with VAT (BT-112) is 2." Send 119.004 and you fail before any arithmetic is even checked.
Three fields are deliberately exempt: unit price (BT-146), quantity (BT-129), and percentages. They may carry as many decimals as you need. That asymmetry is the whole design: high-precision inputs, two-decimal outputs. A telco billing €0.0042 per SMS or a fuel supplier at €1.4729 per litre puts the precision in the price and the quantity, then rounds exactly once when deriving the line net amount:
line net (BT-131) = round₂( quantity × (price / base quantity) + line charges − line allowances )
Interesting footnote: the EN 16931 core rules don't actually verify this line-level equation — the BR-CO chain starts above the line, at BT-106. Peppol BIS 3.0 closes the gap with its own rule, PEPPOL-EN16931-R120, and its schematron implementation grants a tolerance: the check runs through a u:slack(..., 0.02) function, so your stated line net may differ from the recomputed value by ±0.02. That slack exists precisely because the spec doesn't mandate a rounding method — half-up and banker's rounding can legitimately disagree by a cent, and a two-cent window absorbs either choice plus a base-quantity division. Don't lean on it as a license to be sloppy: XRechnung and other CIUSes inherit the rule, and receivers' ERPs often re-check with zero slack.
The bug itself: per-line VAT
Here is the mistake that produces most BR-CO-17 (and consequently BR-CO-15) failures. Nearly every internal invoicing model computes VAT per line, because that's how the invoice is displayed. EN 16931 computes VAT per category: group all lines (and document-level allowances/charges) by VAT category and rate, sum the net amounts into BT-116, and multiply once.
The two approaches disagree. Take three lines of €1.13 net, all at 19% German VAT:
| per-line rounding | per-category (EN 16931) | |
|---|---|---|
| VAT per line | 3 × round₂(1.13 × 0.19) = 3 × 0.21 | — |
| Category taxable (BT-116) | — | 3.39 |
| Category VAT (BT-117) | 0.63 | round₂(3.39 × 0.19) = round₂(0.6441) = 0.64 |
If your system sums the per-line cents and writes 0.63 into BT-117, BR-CO-17 fails: the validator recomputes 3.39 × 0.19 and gets 0.64. If you "fix" it by patching BT-117 to 0.64 but leave a total built from per-line values, BR-CO-15 fails instead. The correct fix is structural: derive the tax breakdown from category sums, not line sums. Keep per-line VAT for display if your users expect it — but the XML's TaxTotal/TaxSubtotal must come from the category computation. This is also why the breakdown is per category and rate: an invoice mixing 19% and 7% lines gets two subtotals, each rounded independently, and BT-110 is their exact (unrounded) sum per BR-CO-14.
Cash rounding is a field, not a fudge
If you round the payable amount for cash payment — Swedish öre rounding is the textbook case, but "round to a friendly number" happens elsewhere too — do not touch the computed totals. EN 16931 gives you a dedicated field: BT-114, the rounding amount (cbc:PayableRoundingAmount in UBL), which enters the chain only at the last step, in BR-CO-16. The Peppol spec's own example: computed total 999.81, desired payable 1000.00 → PayableRoundingAmount = 0.19, amount due 1000.00. Every other total stays mathematically exact. Any other way of getting to a round number — nudging a line, tweaking BT-110 — breaks a BR-CO rule somewhere.
Floats will eventually betray you
One layer below the standard sits an implementation trap: IEEE 754. 0.1 + 0.2 is 0.30000000000000004 in every language whose number is a double, and a validator recomputing your chain in exact decimal arithmetic will catch the discrepancy your float math hid. Two rules of thumb we'd put in any code review:
- Do invoice arithmetic in decimal types (
BigDecimal, Pythondecimal, C#decimal, integer cents) — never binary floats. - Keep money as strings in your JSON payloads (
"amount": "119.00", not"amount": 119.00), so no parser silently converts it to a double on the way through. This is why the FakturWire API represents all monetary values as strings.
Check the math before a receiver does
Totals arithmetic is a document-layer problem, and it's a good illustration of why the document layer and the network layer are different jobs: your Peppol access point will transport an invoice whose totals are off by a cent without blinking — transport-level checks and business-rule validation are separate concerns — and you'll hear about it days later as a rejection from the receiver's system. Validating at document-build time costs one HTTP call:
curl -s -X POST https://fakturwire.com/v1/validate \
-H "Authorization: Bearer $FAKTURWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile": "peppol-bis-3",
"xml": "<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">…</Invoice>"
}'
A one-cent bug comes back as a machine-readable finding, with the rule ID and the XPath of the element that doesn't add up:
{
"valid": false,
"findings": [
{
"rule": "BR-CO-15",
"severity": "error",
"message": "Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + Invoice total VAT amount (BT-110).",
"path": "/Invoice/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount"
}
],
"counts": { "error": 1, "warning": 0 }
}
A response like this is an HTTP 422 — and a 422 is never billed, so validating aggressively in CI is free failure. The checklist version of this whole article:
- Round once per multiplication (line net, category VAT), never on sums.
- Two decimals everywhere except unit price, quantity, and percentages.
- VAT breakdown from category sums, not per-line cents.
- Cash rounding goes in BT-114, nowhere else.
- Decimal arithmetic in code, string-typed money in JSON.
- Validate before you transmit, not after the receiver bounces it.
Try it now: paste an invoice into the free validator — no account needed — or sign up for 50 free credits and put /v1/validate in your CI pipeline, where one-cent bugs cost seconds instead of a payment cycle.
Last verified: 2026-07-29. Sources: Peppol BIS Billing 3.0 specification (rounding section, calculation chain, PayableRoundingAmount example); EN 16931 validation artefacts — model schematron (BR-CO-10…17 rule texts); PEPPOL-EN16931-R120 rule and Peppol UBL schematron (u:slack ±0.02 implementation); cbc:PayableRoundingAmount syntax; Invoice-Portal on decimal limits (BR-DEC-14, BT-146 exemption).