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:

  1. BR-CO-10 — Sum of invoice line net amounts (BT-106) = Σ line net amount (BT-131) over all lines.
  2. BR-CO-13 — Invoice total without VAT (BT-109) = BT-106 − document-level allowances (BT-107) + document-level charges (BT-108).
  3. 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.
  4. BR-CO-15 — Total with VAT (BT-112) = BT-109 + BT-110.
  5. 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, Python decimal, 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).