Multi-rate VAT invoices

One stay can mix VAT rates — a room at 10% and breakfast or a service fee at 20%. TaxLens attributes each line to its own VAT rate, buckets them into per-rate subtotals, and reconciles each one independently so a mis-attributed line is caught instead of silently averaging out.

Why one invoice carries several rates

Accommodation often sits at a reduced VAT rate while ancillary supplies don't. A German folio might charge 7% on the room but 19% on a parking line; an Italian one, 10% on the room and 22% on a minibar charge. The invoice has to show each rate separately — a single blended rate would be wrong and would fail validation.

Per-line attribution with line_item_index

Each tax component is tied to the line it belongs to via line_item_index:

  • line_item_index: null — the room base (the primary accommodation line).
  • line_item_index: 0, 1, … — each entry in line_items[] (a fee, breakfast, a service charge) gets its own index and its own VAT component.

The projection routes each line to its own VAT component, then buckets everything by (category, rate) into one EN 16931 VAT subtotal per distinct rate. Pass line items on the calculation request — see Scenario fields and Tax categories.

{
  "jurisdiction_code": "DE",
  "nights": 2,
  "nightly_rate": 200,           // room → 7%
  "currency": "EUR",
  "line_items": [
    { "item_type": "amenity_fee", "amount": 40, "description": "Parking" }  // → 19%
  ]
}

Each subtotal reconciles on its own

Validation reconciles per (category, rate), not just on the grand total. This enforces two EN 16931 rules at once:

  • BR-CO-10 — the sum of all line net amounts equals the document's total net.
  • BR-S-8 — for each standard-rated subtotal, the taxable base is the sum of exactly the lines at that rate, and the tax equals base × rate.
Why per-rate matters
If a line were attributed to the wrong rate, a grand-total check could still pass — two errors can cancel out. Reconciling each (category, rate) bucket independently catches the mistake, because the misplaced line breaks its bucket's base-times-rate identity.

In the UBL output

The serializer loops the VAT rows into multiple cac:TaxSubtotal elements, and each cac:InvoiceLine carries its own cac:ClassifiedTaxCategory with the right cbc:Percent — so the two-rate split is explicit in the wire format.

Two TaxSubtotals in UBL
<cac:TaxTotal>
  <cbc:TaxAmount currencyID="EUR">35.60</cbc:TaxAmount>
  <cac:TaxSubtotal>
    <cbc:TaxableAmount currencyID="EUR">400.00</cbc:TaxableAmount>
    <cbc:TaxAmount currencyID="EUR">28.00</cbc:TaxAmount>
    <cac:TaxCategory><cbc:ID>S</cbc:ID><cbc:Percent>7</cbc:Percent>
      <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:TaxCategory>
  </cac:TaxSubtotal>
  <cac:TaxSubtotal>
    <cbc:TaxableAmount currencyID="EUR">40.00</cbc:TaxableAmount>
    <cbc:TaxAmount currencyID="EUR">7.60</cbc:TaxAmount>
    <cac:TaxCategory><cbc:ID>S</cbc:ID><cbc:Percent>19</cbc:Percent>
      <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:TaxCategory>
  </cac:TaxSubtotal>
</cac:TaxTotal>

Next

The cross-border B2B reverse charge (category AE on the commission line) is another way one invoice carries multiple categories — see Reverse charge. For the standards behind these subtotals, revisit EN 16931, UBL & PEPPOL.