Engineering post, not tax advice. I am a developer, not your accountant. Everything below is about where the branch points are in your checkout code — confirm the actual numbers with someone qualified before you invoice anyone.
You sell a $100 digital product. Same product, same price, same Stripe account. Three different buyers, three completely different correct answers — and only one of them is “add tax.”
Buyer one: a person in Germany
You are an Irish company. They are a consumer in Berlin.
GET /decide?seller_country=IE&buyer_country=DE&product_type=digital
{
"chargeVat": true,
"mechanism": "standard",
"rate": 19,
"buyerType": "consumer",
"invoiceNote": "Charge DE VAT at 19% (customer's country) and report via the One-Stop Shop (OSS).",
"legalBasis": "B2C electronically supplied services taxed where the customer belongs (Art. 58)"
}
Enter fullscreen mode Exit fullscreen mode
Note the rate is German, not Irish. For digital goods sold to EU consumers, the place of supply is where the customer is. Your own country’s rate is irrelevant. This is the one most homegrown checkouts get wrong — they apply the seller’s rate because that is the single number someone hardcoded.
That also means you do not need one VAT rate. You need twenty-seven, and they change.
Buyer two: a company in Germany
Same country. Same product. They type a VAT number into your checkout.
GET /decide?seller_country=IE&buyer_country=DE&buyer_vat_id=DE811907980&product_type=digital
{
"chargeVat": false,
"mechanism": "reverse_charge",
"rate": 0,
"buyerType": "business",
"vatId": { "valid": true, "country": "DE", "prefix": "DE" },
"invoiceNote": "No VAT charged. Reverse charge applies — the customer accounts for VAT in their country."
}
Enter fullscreen mode Exit fullscreen mode
One field in a form moved the tax from 19% to zero, and changed what has to be printed on the invoice. If your checkout has no VAT-number field, every EU business customer is being overcharged and every invoice you send them is wrong.
Worth knowing: format-checking a VAT number is not the same as verifying it is real and active. That needs a VIES lookup. Treat the format check as a cheap first gate, not proof.
Buyer three: a person in the United States
GET /decide?seller_country=IE&buyer_country=US&product_type=digital
{
"chargeVat": false,
"mechanism": "out_of_scope",
"rate": 0,
"invoiceNote": "No EU VAT charged — the customer is outside the EU (place of supply is where the customer belongs)."
}
Enter fullscreen mode Exit fullscreen mode
Not zero-rated. Out of scope — a different thing, and it lands differently on your return. “Zero” in your database is hiding at least three distinct states: taxed at 0%, exempt, and never in scope to begin with. If your schema stores a rate and nothing else, you have thrown away the reason, and the reason is what an audit asks for.
The one that surprises people
Ask for the digital-goods rate in California:
GET /rate?country=US®ion=CA&product_type=digital
{
"country": "US", "region": "CA",
"taxType": "sales_tax",
"rate": 0,
"taxable": false,
"notes": [
"Digital goods are generally NOT taxed...",
"State-level rate only; local/city/district rates are out of scope. Confirm nexus and local rules with the state Department of Revenue."
]
}
Enter fullscreen mode Exit fullscreen mode
California — the state everyone assumes is the aggressive one — generally does not tax digital goods. Meanwhile plenty of smaller states do. There is no rule of thumb here; product type and state interact, and guessing produces confident wrong answers in both directions.
And note the second caveat, which is the one that actually bites US sellers: state rate is not the whole rate. Local and district rates stack on top, and whether you owe anything at all depends on nexus — a question about your business, not about the buyer.
What this means for your code
The useful takeaway is not a number, it is a shape:
- Tax is a decision, not a lookup. The inputs are seller country, buyer country, buyer region, product type, and whether the buyer produced a valid business ID. A function that takes only “country” cannot be correct.
-
Store the reasoning with the amount.
mechanism: "reverse_charge"and a legal basis are what let you answer a question a year later. A bare0.00is unfalsifiable. - Generate the invoice line at decision time. The wording differs between reverse charge and out of scope, and reconstructing it later means re-deriving the decision.
- Rates rot. Whatever you use, know when it was last reviewed.
The thing I built
I kept rewriting this branch logic, so it is now a small self-contained API: EU-27 + UK VAT, US state-level sales tax, Canada GST/HST/PST, Australia GST. Runs on Cloudflare Workers, pure compute, no external calls, and returns the legalBasis and invoiceNote shown above.
Free to hit, no key — every response in this post is a live call you can run right now:
- https://checkout-tax-api.pages.dev/decide?seller_country=IE&buyer_country=DE&product_type=digital
- https://checkout-tax-api.pages.dev/openapi.json
Disclosure, since it is mine: if you would rather run tax logic inside your own infrastructure than call someone else’s service — a very reasonable thing to want for something this load-bearing — the source kit is $49: https://craniusmaximus.gumroad.com/l/zxnvil. The hosted API stays free regardless, and nothing here depends on buying anything.
Whatever you use, including your own code: make it return why. The rate is the easy half.
Related: the same question answered for all 27 EU countries and all 50 US states — B2C rate, B2B reverse charge, and which states tax digital goods at all. Generated from live API calls, not written from memory.
답글 남기기