Switzerland has three VAT rates: 8.1% (standard), 2.6% (reduced, for food, books, medicines, and newspapers), and 3.8% (special rate for hotel and accommodation). Many businesses deal with more than one of these on a single invoice — a hotel billing room nights and breakfast separately, a retailer selling food and non-food items together, or a healthcare supplier mixing taxable and exempt services.
The EN 16931 standard — which underpins PEPPOL BIS Billing 3.0, ZUGFeRD, and Factur-X — handles this cleanly. But the implementation trips up a lot of developers because the tax model requires specific structures that differ from what most accounting software produces internally.
How the tax model works
The core rule is this: each invoice line declares which VAT rate applies to that line. At the document level, all lines with the same rate are consolidated into a single tax subtotal. One subtotal per distinct rate — not one per line, not one per product category.
If an invoice has 10 lines at 8.1% and 5 lines at 2.6%, the document has two TaxSubtotal blocks: one summing all the 8.1% lines, one summing all the 2.6% lines. The TaxTotal sums both subtotals into a single total VAT figure.
Line-level tax declaration
Each invoice line carries its own tax category and rate:
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="CHF">48.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Organic apple juice 1L</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>2.6</cbc:Percent>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="CHF">24.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="CHF">35.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Kitchen utensil set</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>8.1</cbc:Percent>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="CHF">35.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
Both lines use category code S (standard-rated VAT). The rate value in Percent is what distinguishes them — 2.6 for the food item, 8.1 for the non-food item.
Consolidating into tax subtotals
After all lines, the TaxTotal block consolidates by rate:
<cac:TaxTotal>
<cbc:TaxAmount currencyID="CHF">4.08</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="CHF">48.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="CHF">1.25</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>2.6</cbc:Percent>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="CHF">35.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="CHF">2.84</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>8.1</cbc:Percent>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
The TaxAmount in TaxTotal (4.08) must equal the sum of the two subtotal amounts (1.25 + 2.84 = 4.09 — note the one-cent rounding difference here, covered below).
Each TaxSubtotal/TaxableAmount is the sum of all LineExtensionAmount values for lines at that rate.
Rounding and the one-cent problem
This is where multi-rate invoices get tricky. If you calculate tax for each subtotal independently and round to two decimal places, rounding errors accumulate. On a two-rate invoice the error is usually small, but on invoices with many lines and three rates it can add up to several cents, which causes the Schematron consistency check to fail.
The right approach:
- Sum the
LineExtensionAmountvalues for each rate group without rounding — keep full precision internally. - Calculate tax for each group:
taxableAmount × rate / 100, again without rounding. - Round only when writing the final values into the XML.
- Sum the rounded subtotal
TaxAmountvalues to getTaxTotal/TaxAmount— do not recalculate from scratch.
If there is still a one-cent discrepancy between TaxTotal/TaxAmount and the sum of the subtotals due to rounding, EN 16931 allows the total to be the sum of the rounded subtotals (not independently recalculated). Most Schematron validators apply a tolerance of ±0.02 CHF before failing the check.
Mixing taxed and exempt lines
A hotel invoice might combine taxable room charges (3.8%) with exempt medical or educational services. Exempt lines use tax category E instead of S, with Percent set to 0 and a mandatory exemption reason:
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="CHF">200.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="CHF">0.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>E</cbc:ID>
<cbc:Percent>0</cbc:Percent>
<cbc:TaxExemptionReason>Exempt under Art. 21 MWSTG</cbc:TaxExemptionReason>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
The exempt subtotal still appears in TaxTotal — it contributes a taxable amount (for reporting) but zero tax. The TaxTotal/TaxAmount is still only the sum of actually charged VAT.
For a full explanation of when to use E, Z, AE, and the other category codes, the tax category codes post covers each one.
The 3.8% hospitality rate in practice
The 3.8% rate applies specifically to hotel and accommodation services — room charges, breakfast, and other overnight stay services. It does not apply to restaurant meals at a hotel (those are 8.1%), nor to conference facilities sold separately.
If you run a hotel and your invoicing system was not designed with Swiss VAT in mind, the 3.8% rate is often the one that gets missed. Many ERP templates only have slots for two rates. Check that your invoice generator produces a valid TaxSubtotal for 3.8% and does not accidentally roll it into the 8.1% bucket.
In the XML, the 3.8% rate is represented exactly as 3.8 in Percent. The category code is still S. The hospitality VAT post covers the Swiss rules on which services qualify for this rate.
What the LegalMonetaryTotal looks like with multiple rates
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="CHF">283.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="CHF">283.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="CHF">287.08</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="CHF">287.08</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
LineExtensionAmount is the sum of all line amounts regardless of rate. TaxExclusiveAmount equals LineExtensionAmount when there are no header-level charges or allowances. TaxInclusiveAmount = TaxExclusiveAmount + TaxTotal/TaxAmount. The Schematron verifies this relationship, so any inconsistency here will produce a BR-CO-15 error.