Tax is the part of building a SaaS that every developer ignores until a customer from Germany emails asking for a VAT invoice, or until you realize you've been selling to Californians for 18 months without collecting sales tax. We ignored it too. Then we didn't. Here's what we learned.
The good news: Stripe Tax is genuinely excellent at the hard parts — figuring out what rate applies where, keeping up with changing regulations, and generating proper invoices. The bad news: you still need to understand what you're turning on, because 'automatic' doesn't mean 'zero configuration' and it definitely doesn't mean 'zero liability.'
What Stripe Tax Actually Does (and Doesn't Do)
Stripe Tax calculates and collects tax on your transactions. It covers VAT in the EU and UK, GST in Australia, Canada, New Zealand, and a bunch of others, plus US sales tax across all 50 states. It figures out the right rate based on the customer's location, your product type, and whatever regulatory rules apply. It also handles tax-exempt customers if you pass in the right data.
What it doesn't do: register you for tax in any jurisdiction. That's still on you. Stripe will calculate and collect German VAT all day long, but if you haven't registered for the EU OSS (One Stop Shop) scheme, you're collecting money you have no legal framework to remit. Stripe Tax is the calculation and collection engine — you're still the business that needs to be compliant.
Stripe Tax calculates. You remit. Know the difference before you turn it on.
Also worth knowing: Stripe Tax costs 0.5% per transaction where it's used (or 0.5% of the taxable amount on transactions where you use automatic tax). That's on top of your normal Stripe fees. For most SaaS businesses this is completely worth it — the alternative is paying a tax consultant thousands of dollars a year to do manually what Stripe does automatically. But it's not free.
Enabling Stripe Tax on Subscriptions
If you're using Stripe Billing for subscriptions, enabling tax is mostly an API flag plus making sure you're capturing the customer's address. Here's the core of it:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// When creating a customer, always capture their address
const customer = await stripe.customers.create({
email: user.email,
name: user.name,
address: {
line1: billingAddress.line1,
city: billingAddress.city,
state: billingAddress.state, // critical for US sales tax
postal_code: billingAddress.zip,
country: billingAddress.country, // ISO 3166-1 alpha-2, e.g. 'DE', 'US'
},
tax: {
// Let Stripe auto-detect tax location from address
// You can also set ip_address here for additional validation
},
});
// When creating a subscription, enable automatic tax
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
automatic_tax: {
enabled: true,
},
// Optionally expand to see what tax was calculated
expand: ['latest_invoice.payment_intent'],
});
The `automatic_tax: { enabled: true }` flag is what activates Stripe Tax for that subscription. Every invoice generated will then include calculated tax based on the customer's address. If you forget the address, Stripe can't calculate tax and will either skip it or throw an error depending on your settings — so make sure you're capturing billing address in your checkout flow.
Setting Up Your Products Correctly
This is where most people mess up. Stripe Tax needs to know what kind of product you're selling to apply the right rate. Software, physical goods, and services are taxed completely differently depending on the jurisdiction. In the EU, B2B SaaS (digital services) uses the reverse charge mechanism. In the US, SaaS is taxable in some states and exempt in others. You need to tell Stripe what you're selling.
You do this by setting a tax code on your product. Stripe has a full list at stripe.com/docs/tax/tax-codes but for most SaaS products you want:
- txcd_10103001 — SaaS (Software as a Service) — this is the right one for most B2B tools
- txcd_10103000 — Cloud services / IaaS if you're selling compute
- txcd_99999999 — General services (fallback, less accurate)
- txcd_10402000 — Digital goods / downloadable software if you sell licenses
// Set the tax code when creating a product
const product = await stripe.products.create({
name: 'Pro Plan',
tax_code: 'txcd_10103001', // SaaS
});
// Or update an existing product
await stripe.products.update('prod_xxxxx', {
tax_code: 'txcd_10103001',
});
// You can also set it at the price level if the same product
// has items with different tax treatments (rare for SaaS)
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2900,
currency: 'usd',
recurring: { interval: 'month' },
// tax_code here overrides the product-level code
});
If you don't set a tax code, Stripe Tax will make its best guess based on context, but its best guess might be wrong and wrong tax rates on invoices is exactly the kind of thing that causes problems in an audit. Spend 10 minutes setting the right tax code and sleep better.
Handling B2B Customers and VAT Numbers
If you have B2B customers in the EU, they'll want to provide their VAT number so they can handle VAT themselves (the reverse charge mechanism). You need to collect this at checkout and pass it to Stripe. This is a real thing your EU business customers will ask about, and if you don't support it they'll get annoyed.
// Add a VAT number to a customer
// Stripe will validate it against the EU VIES database
await stripe.customers.createTaxId(customerId, {
type: 'eu_vat', // or 'gb_vat', 'au_abn', 'ca_gst', etc.
value: 'DE123456789', // the actual VAT number they provided
});
// You can also retrieve tax IDs for a customer to display them
const taxIds = await stripe.customers.listTaxIds(customerId);
// When Stripe sees a valid EU VAT number + EU address,
// it will automatically apply the reverse charge mechanism
// on invoices — no additional config needed
// In your checkout form, collect this optionally:
// "VAT Number (EU businesses only, optional)"
// Then call this after customer creation if they provided one
The `type` field changes based on the country. UK businesses use `gb_vat`, Australians use `au_abn`, Canadians use `ca_gst` or `ca_pst` depending on the province. Stripe's docs have the full list. The validation is nice — Stripe will actually verify the VAT number is real before accepting it, which saves you from customers who type random numbers.
Checkout Sessions: The Easier Path
If you're using Stripe Checkout (the hosted payment page) instead of building your own checkout UI, enabling tax is almost embarrassingly simple. Stripe Checkout will even show customers the calculated tax amount before they pay, which is legally required in some jurisdictions.
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId, // if existing, or let Stripe create one
line_items: [
{
price: priceId,
quantity: 1,
},
],
automatic_tax: {
enabled: true,
},
// Collect billing address for tax calculation
// 'auto' collects based on what Stripe Tax needs
// 'required' always forces collection
billing_address_collection: 'required',
// Let customers enter their VAT number in checkout
tax_id_collection: {
enabled: true,
},
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
The `tax_id_collection: { enabled: true }` is worth turning on even if you're not sure you need it. It adds a small optional field to checkout where business customers can enter their VAT/GST number. If they do, Stripe handles the reverse charge automatically. If they don't, no problem. There's no reason not to enable it.
The Tax Dashboard and Reporting
Once you have transactions flowing through Stripe Tax, the dashboard at dashboard.stripe.com/tax gives you a breakdown of tax collected by jurisdiction. This is what your accountant needs when it's time to file. For EU sellers using the OSS scheme, you'll see it broken down by EU member state. For US sellers, you'll see it by state.
Stripe also has tax report exports (CSV) that are formatted for common filing requirements. We won't pretend this fully automates filing — you still need to actually submit the returns and remit the money. But compared to manually tracking this in a spreadsheet, which is what we were doing before, it's night and day. The data is clean, accurate, and timestamped.
Stripe Tax + a good accountant who understands SaaS = you can stop worrying about tax and ship features instead.
One thing to watch: the threshold monitoring feature. Stripe Tax can tell you when you're approaching economic nexus thresholds in US states (typically $100k in sales or 200 transactions). This is useful because you don't need to register and collect tax in a state until you hit nexus there — and Stripe will warn you before you do, so you can get registered in time.
Common Mistakes We've Seen (and Made)
- Enabling automatic_tax without capturing customer addresses — Stripe will skip tax calculation silently in some configurations
- Not setting tax codes on products — letting Stripe guess works until it doesn't, and 'doesn't' usually means an audit
- Turning on Stripe Tax for jurisdictions where you haven't registered — you'll collect tax with no legal way to remit it
- Not enabling tax_id_collection on checkout — B2B EU customers will complain, loudly, by email
- Forgetting to update existing subscriptions when you enable tax — new subscriptions get it, old ones don't unless you explicitly update them
- Treating Stripe Tax as complete tax compliance — it's a tool, not a tax advisor
That last point about existing subscriptions is something that bit us. When you first enable Stripe Tax, it only applies to new transactions. Your existing subscribers' invoices won't automatically start including tax. You need to update those subscriptions:
// Update existing subscriptions to enable automatic tax
// Run this as a migration script — carefully, in batches
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100,
});
for (const sub of subscriptions.data) {
if (!sub.automatic_tax.enabled) {
await stripe.subscriptions.update(sub.id, {
automatic_tax: { enabled: true },
// proration_behavior: 'none' so you don't create proration invoices
proration_behavior: 'none',
});
// Add a small delay to avoid rate limits
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// This only affects the next invoice — it won't retroactively
// add tax to past invoices, which is correct behavior
Run this carefully. Test it on one subscription first. Don't do it at 2am the night before a board meeting. We're speaking from experience on that last one, though to be fair the 2am deploys have taught us most of what we know.
If you're starting a new project and want all of this wired up from day one — tax handling, Stripe subscriptions, billing portal, the whole thing — our templates at peal.dev include Stripe Tax integration as part of the payments setup, so you're not building this from scratch every time.
The Practical Checklist
Here's the actual sequence to follow if you're adding Stripe Tax to an existing SaaS:
- Talk to an accountant first — understand where you actually have tax obligations before collecting anything
- Register for tax in the relevant jurisdictions (EU OSS, US states where you have nexus)
- Enable Stripe Tax in your dashboard settings and set your origin address
- Set tax codes on all your products (txcd_10103001 for SaaS)
- Update your checkout flow to collect billing addresses and optionally tax IDs
- Enable automatic_tax on new subscription creation
- Run the migration script to enable it on existing subscriptions
- Verify a test transaction shows the right tax on the invoice
- Set up a recurring calendar reminder to review the tax dashboard and file returns
Tax is genuinely one of those things where doing it right early is 10x cheaper than cleaning up the mess later. Stripe Tax removes most of the calculation complexity — the part that's left is just organizational: register where you need to, file on time, and keep your product tax codes accurate. That's actually manageable. Accountants love it when you show up with clean Stripe Tax exports instead of a folder of manual spreadsheets.
