---
title: VAT and reverse charge
path: concepts/vat-and-reverse-charge
status: published
---

# VAT and reverse charge

ScaiControl ships with EU-compliant VAT determination. At invoice finalisation, the system looks at the seller's country, the buyer's country, the buyer's VAT number, and whether the buyer is a business, and picks the right tax treatment. This page covers the rules and the data model.

## The four rules

`services/billing/vat.py:determine_vat()`:

1. **Non-EU buyer** → 0% VAT, no reverse charge, legal note `"Export outside the EU - VAT not applicable"`.
2. **Seller not in EU** (edge case) → 0% VAT, no note, label `"No VAT"`.
3. **Domestic** (seller country == buyer country) → seller's standard rate (NL 21%, DE 19%, FR 20%, etc.), no reverse charge.
4. **Intra-EU B2B with valid VAT number** (different EU countries, `is_business=True`, `vat_number` set) → 0%, **reverse charge**, legal note `"Reverse charge - Art. 196 EU VAT Directive"`.
5. **Intra-EU B2C** (no VAT number or not a business) → seller's country rate (per OSS rules, simplification).

The result is a `VATDetermination` dataclass:

```python
@dataclass(frozen=True)
class VATDetermination:
    tax_rate: Decimal           # e.g. Decimal("21.00")
    reverse_charge: bool
    tax_label: str              # "VAT 21.00%" | "VAT 0% (Reverse Charge)" | …
    legal_note: str | None
```

## Standard rates

The lookup table `EU_STANDARD_VAT_RATES` in `vat.py` holds the current rates for all 27 EU member states. Excerpt:

| Country | Rate |
|---|---|
| NL | 21.00% |
| DE | 19.00% |
| FR | 20.00% |
| BE | 21.00% |
| ES | 21.00% |
| IT | 22.00% |
| AT | 20.00% |
| PT | 23.00% |
| HU | 27.00% |

Reduced rates (e.g. NL 9% for books) are not auto-applied — the standard rate is the default. To bill at a reduced rate, set `tax_rate` explicitly on the line item before finalisation.

## Per-line categories

When VAT rules apply differently across an invoice's lines (rare but legal), each `invoice_line_items` row carries its own:

- `tax_category` (UNCL 5305):
  - `S` — standard rate
  - `AE` — VAT reverse charge
  - `G` — VAT free for export
  - `E` — exempt from VAT
  - `Z` — zero-rated
  - `O` — services outside scope
  - `K` — VAT exempt for EEA intra-community supply

- `tax_rate` (Numeric(5,2)) — explicit percentage, or null to inherit the invoice-level rate.
- `tax_cents` — computed at finalisation as `amount_cents * rate / 100`.

Most line items leave `tax_category` and `tax_rate` null; the invoice-level VAT determination fills them in based on `determine_vat()`. Setting them explicitly is the override path for special cases (e.g. one line of a domestic invoice covers a service that's locally exempt).

## The frozen audit trail

Once finalised, the per-rate breakdown is stored on the invoice as `vat_details.entries[]`:

```json
{
  "entries": [
    {
      "category": "S",
      "rate": "21.00",
      "label": "VAT 21.00%",
      "taxable_cents": 15000,
      "amount_cents": 3150,
      "legal_note": null
    }
  ]
}
```

The `label` and `legal_note` are stored in English (canonical). The render layer localises them per the buyer's `preferred_language` — see [Templates](./templates) for how that flows through.

## Localised legal notes

When `reverse_charge=True`, the legal note shipped on the invoice is mandatory in EU law. ScaiControl ships translations for the five primary EU operating languages:

| Language | Reverse charge legal note |
|---|---|
| EN | Reverse charge - Art. 196 EU VAT Directive |
| NL | BTW verlegd – Art. 196 EU BTW-richtlijn |
| DE | Steuerschuldnerschaft des Leistungsempfängers – Art. 196 EU-MwSt-Richtlinie |
| FR | Autoliquidation de la TVA – Art. 196 de la directive TVA UE |
| ES | Inversión del sujeto pasivo – Art. 196 de la Directiva del IVA de la UE |
| IT | Inversione contabile dell'IVA – Art. 196 Direttiva IVA UE |

If the buyer's `preferred_language` isn't in this list, the English canonical text is used.

The export-outside-EU note is similarly localised.

## VAT validation

ScaiControl does NOT call VIES (the EU VAT number validation service) at finalisation. Validating each invoice would add a hard network dependency and a quota concern. Instead:

- Buyers are expected to register their VAT number once on their billing profile.
- The operator should periodically validate via VIES (or accept the buyer's certification) and update `tax_exempt` on `tenant_billing_profiles` if the number's invalid.

A future enhancement is an admin "validate VAT" button per tenant that hits VIES on demand and stores the result.

## Reduced and exempt cases

To bill at a non-standard rate or exempt a line:

```python
# In line_items input to POST /admin/invoices:
{
  "description": "Educational training package",
  "quantity": 10, "unit_price_cents": 5000,
  "amount_cents": 50000,
  "line_type": "manual",
  "tax_category": "E",        # exempt
  "tax_rate": "0.00",
}
```

For the entire invoice to apply reverse charge regardless of `determine_vat()` (e.g. mixed-jurisdiction services), set every line's `tax_category` to `AE` and `tax_rate` to `0.00`, and override `invoice.reverse_charge = True`.

## Bottom line

For 99% of EU billing, the four rules above are correct out of the box. The edge cases (reduced rates, partial exemptions, OSS thresholds for B2C beyond €10k) need operator judgement — ScaiControl exposes the knobs but doesn't enforce them.
