EU tax logic in SaaS billing: the order of decisions
Most tax errors in SaaS billing don't come from a wrong rate in a table. They come from asking the questions in the wrong order. Ask for the country first and look up the rate next, and you'll charge French VAT to a French company with a valid VAT ID. Evaluate the payment method first, and you've let the payment provider decide the place of supply.
This article is about the architecture: which decisions are made in which order, how the result is stored, and how to test a rule change before it goes live. The examples assume a seller established in Germany that sells electronically supplied services, but the decision order is the same for any EU seller.
Five questions, always in the same order
- Who is the customer? A business or a consumer, and what evidence supports that classification.
- Where is the supply taxable? For businesses, generally where the customer is established. For consumers buying electronic services, where they live, provided the EU-wide EUR 10,000 threshold has been exceeded or you've opted out of it (Article 58 of the EU VAT Directive).
- Who owes the VAT? For supplies to businesses in other EU countries, the customer (reverse charge). Otherwise the seller.
- Which rate applies? Only now, and only if the seller owes the VAT: the rate of the place of supply for the product's tax category, valid on the supply date. The Commission publishes the standard and reduced VAT rates by member state, but your rate table needs validity dates of its own.
- How is it reported, and what goes on the invoice? Domestic VAT return, OSS return or EC Sales List, plus the matching invoice wording.
Each question needs the answer to the one before. The rate comes fourth because a reverse charge customer doesn't need one at all. Reporting comes last because it follows entirely from the first four answers. If your code derives the filing route from anything other than those answers, that's where your next reconciliation difference will come from.
The decision table for a German seller
The five questions produce a small table. Write it down for your business, have your tax adviser sign it off, and only then write code.
| Customer | Customer country | Scheme | VAT on the invoice | Reported in |
|---|---|---|---|---|
| Business | Germany | Domestic | German VAT | Domestic return |
| Business, VAT ID verified | Other EU country | Reverse charge | None, with note | EC Sales List |
| Business, proof missing | Other EU country | Your policy: hold or manual approval | Open | Open |
| Consumer | Germany | Domestic | German VAT | Domestic return |
| Consumer, threshold exceeded | Other EU country | OSS | VAT of the customer's country | OSS return |
| Consumer, below threshold | Other EU country | Domestic | German VAT | Domestic return |
| Any | Outside the EU | Not taxable in Germany | No German VAT | Check obligations in that country separately |
The third row is the one most systems lack. A customer who says they're a business but whose VAT ID hasn't been confirmed mustn't slide silently into row two. Which treatment you pick instead is a trade-off between conversion and risk. What matters is that the row exists explicitly. How the check itself should run is covered in the article on automating VIES checks.
Row two requires a reverse charge statement on the invoice and an entry in the EC Sales List. The details are in the article on creating a reverse charge invoice.
Store the result as a decision object
The five answers belong in an object of their own, stored per invoice line and only referenced by the invoice. A sketch:
{
"tax_decision_id": "td_7f3a",
"inputs": {
"customer_status": "BUSINESS_VERIFIED",
"vat_id_check_ref": "chk_1029",
"customer_country": "FR",
"product_tax_category": "ELECTRONIC_SERVICE",
"supply_date": "2026-10-01"
},
"outputs": {
"place_of_supply": "FR",
"tax_liable": "CUSTOMER",
"scheme": "REVERSE_CHARGE",
"rate": null,
"invoice_note": "REVERSE_CHARGE_NOTE",
"report": "EC_SALES_LIST"
},
"rule_version": "tax-rules@41",
"decided_at": "2026-10-01T06:12:44Z"
}
Three properties make the object useful. It stores inputs and outputs, so the decision can be explained without re-running the rules. It carries the rule version, so a later change doesn't reinterpret old invoices. And it's the only source for everything downstream: invoice PDF, e-invoice, ledger export and filing all read the same fields. If the PDF shows a reverse charge note while the ledger export posts to a taxable-sales account, two components are reading from different sources.
The product's tax category belongs in the versioned product catalogue, not in checkout code. How to version products and prices so old invoices keep their category is covered in the article on product catalogue versioning.
When to decide again, and when not to
A tax decision is made when an invoice line is finalised. The checkout preview may call the same logic, but it's only a preview: on finalisation the decision is made again with the data valid at that moment.
New decisions are triggered by a successful or failed VAT ID check, an address change, a new version of the rate table or a new product. Each of these changes gets an effective date and only affects lines finalised after it.
Finalised lines never get a new decision. A credit note inherits the decision of the line it corrects instead of making its own. A pro-rated mid-month upgrade produces two lines, each with its own decision, if the customer's status changed in between. If an old invoice really does need different tax treatment, that happens through a new document with an approval, never through a silent re-run.
Test a rule change before it goes live
Because the rules are versioned, they can be tested like code. Two tests are enough to start with.
First, a fixed set of reference cases with expected results: one per row of the decision table plus a few edge cases. A consumer whose invoice is the one that crosses the threshold; a business whose VAT ID check got no answer because the service was down; an address change in the middle of a billing period; a credit note against a line from the previous quarter. Finance writes the expected results, engineering turns them into automated tests.
Second, a replay: run the new rule version against the stored inputs of every decision from last quarter and compare the results with the stored outputs. Every difference is either intended, because the rule is meant to change from a certain date, or a bug. Unintended differences must be zero before the version goes live.
Where to start
First, take ten real invoice lines from different countries and customer types and check whether your system can answer this today: which of the five decisions was made, on which input, with which rule version? If the answer for any line only comes from recalculating, the stored decision object is missing. Build it before any other tax feature.
Second, search your code for places where a VAT rate is read without first asking about customer status and who owes the tax. Each one is a candidate for a reverse charge error.
Kontier stores the tax decision with its inputs and rule version on every invoice line and exposes it through its billing API.
Not tax or legal advice. This post describes the law and common practice in general terms. For your own case, ask your tax adviser. As of: