Tax is the part of SaaS that everyone ignores until a customer from Germany asks why there's no VAT on their invoice, or until you realize you've been selling to California residents for 18 months without collecting sales tax. We've been there. It's not fun. Stripe Tax exists precisely to prevent that panic, but it's not a magic checkbox — there are real things you need to understand before you flip it on and call it done.
This post covers how Stripe Tax actually works, the gotchas we hit building subscription billing flows, and the code you need to collect tax correctly without accidentally charging customers the wrong amount or breaking your checkout.
What Stripe Tax Actually Does
Stripe Tax automatically calculates and collects the right amount of tax based on where your customer is and what you're selling. It covers VAT (EU, UK, Australia, etc.), GST (Canada, Singapore, Australia again), and US sales tax across all states where you have nexus. It also handles tax registration tracking — meaning it tells you when you're approaching thresholds that would require you to register in a new jurisdiction.
What it does NOT do: file your taxes for you, register you with tax authorities, or handle everything in every country automatically. You still need to register in each jurisdiction yourself. Stripe Tax collects the money and gives you the reporting data. What you do with that data is still your problem — or your accountant's problem, depending on your budget.
Stripe Tax collects and calculates. Filing and registering is still on you. Don't confuse the two — that's how you end up with a tax liability you didn't know about.
The Tax Code Problem Nobody Warns You About
Before you enable automatic tax on your products, you need to assign a tax code to each one. This is where most people mess up. Stripe has hundreds of tax codes, and the one you pick determines how tax is calculated in different jurisdictions. Software-as-a-Service products have different tax treatment than digital goods, which are different from professional services, which are different from physical goods.
For most SaaS products, you want txcd_10103001 (SaaS — cloud services) or txcd_10402000 (software subscription). These aren't interchangeable — in some US states, one is taxable and the other isn't. If you pick the wrong one, you're either over-collecting tax (annoying customers) or under-collecting it (your problem at filing time).
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// When creating a product, always set the tax code
const product = await stripe.products.create({
name: 'Pro Plan',
tax_code: 'txcd_10103001', // SaaS - cloud services
});
// When creating a price, mark it as tax inclusive or exclusive
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2900,
currency: 'usd',
recurring: { interval: 'month' },
// tax_behavior tells Stripe whether tax is on top of the price
// or already included in it
tax_behavior: 'exclusive', // price shown is before tax
});
// For EU customers, you typically want 'inclusive' so the price
// shown to them already includes VAT (legally required in many EU countries)The tax_behavior field on prices is critically important. 'exclusive' means the $29 you charge is $29 plus whatever tax is owed — the customer pays more than advertised. 'inclusive' means tax is baked into the $29, so the customer always pays exactly what you showed them. For EU/UK customers, inclusive is often legally required. For US customers, exclusive is the norm. If you're selling globally, you might need different prices per region, which gets messy fast.
Enabling Automatic Tax on Checkout Sessions
Once your products have tax codes and your prices have tax behavior set, enabling tax on a Checkout Session is one line. But there are a few surrounding things you need to get right.
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const { priceId, customerId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId,
line_items: [
{
price: priceId,
quantity: 1,
},
],
// This is all you need to enable automatic tax
automatic_tax: { enabled: true },
// IMPORTANT: Stripe needs to know the customer's location
// to calculate tax. Collect their address during checkout.
billing_address_collection: 'required',
// For B2B sales, allow customers to enter a VAT number
// Stripe will validate it and apply reverse charge if valid
customer_update: {
address: 'auto',
name: 'auto',
},
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing`,
});
return NextResponse.json({ url: session.url });
}The billing_address_collection: 'required' part is non-negotiable. Stripe can't calculate tax without knowing where the customer is. If you skip this and a customer checks out without an address, Stripe will either skip tax entirely or error out depending on your settings. Neither is what you want.
For B2B sales where your customer might be a VAT-registered business in Germany, setting customer_update with address: 'auto' means Stripe will save the address to the customer object after checkout. This is useful because on future invoices (subscription renewals, etc.), Stripe will use the saved address to keep calculating tax correctly without asking for it again.
Tax on Subscriptions After Checkout
Here's where things get interesting. When a customer subscribes, tax isn't just calculated once — it's calculated on every renewal invoice. If your customer moves from one state to another, or if tax rates change, Stripe Tax handles that automatically. The tax calculation happens fresh each billing cycle.
But you need to make sure automatic_tax is enabled on the subscription itself, not just the checkout session. When you create a subscription directly (not through Checkout), you have to set it explicitly:
// Creating a subscription directly with automatic tax
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
automatic_tax: { enabled: true },
});
// If you need to enable it on an existing subscription
// (e.g., you're migrating from manual tax)
const updated = await stripe.subscriptions.update(subscriptionId, {
automatic_tax: { enabled: true },
});
// To read the tax breakdown on an invoice
const invoice = await stripe.invoices.retrieve(invoiceId, {
expand: ['total_tax_amounts.tax_rate'],
});
const taxBreakdown = invoice.total_tax_amounts?.map(item => ({
amount: item.amount, // in cents
taxable_amount: item.taxable_amount,
rate: item.tax_rate, // includes jurisdiction, percentage, etc.
inclusive: item.inclusive, // whether this was baked into the price
}));The expand on invoice retrieval is important if you want to show customers a detailed tax breakdown on your own invoices or receipts. Stripe's hosted invoices show this automatically, but if you're building custom invoice PDFs or a billing page in your app, you'll need to fetch and display this data yourself.
The Nexus Question: When Do You Even Need to Collect Tax?
Nexus is the legal concept of having enough presence in a jurisdiction that you're required to collect tax there. In the US, this gets complicated fast. Every state has different thresholds — some say you need nexus after $100k in sales, some say 200 transactions, some say both. EU VAT applies once you hit €10,000 in cross-border sales within the EU.
Stripe Tax has a threshold monitoring feature that tracks your sales by jurisdiction and warns you when you're approaching registration thresholds. You can find this in the Stripe Dashboard under Tax > Registrations. It won't automatically register you (you have to do that yourself or use a service like Anrok or TaxJar for full automation), but it at least tells you when to panic.
- US sales tax: Collect only in states where you've registered. Stripe Tax won't collect in states you haven't registered in.
- EU VAT: Once registered, Stripe Tax handles all 27 EU member states with the correct rates. Use OSS registration in one EU country to cover all of them.
- UK VAT: Separate from EU post-Brexit. Register with HMRC, tell Stripe your UK VAT number, and it handles the rest.
- Australia GST: 10% on digital services to Australian consumers if you earn over AUD 75,000/year from Australian customers.
- Canada GST/HST: Federal GST plus provincial taxes. Stripe Tax handles the calculation once you're registered.
The actual registration process is tedious. For the EU, OSS (One Stop Shop) registration in one EU country covers you for all B2C sales across the EU — you file one return per quarter. This is manageable. The US is the nightmare — if you get big enough, you may end up registered in 30+ states, each with their own filing schedules. That's when you pay someone else to handle it.
Where Stripe Tax Actually Falls Short
We've been pretty positive about Stripe Tax so far, so let's be honest about the pain points. First, the 0.5% fee on taxable transactions adds up. If you're processing high volume, this is real money. Evaluate whether that's worth the automation vs. a manual approach or a dedicated tax service.
Second, marketplace and multi-vendor scenarios are not handled well. If you're building a platform where other sellers transact, Stripe Tax doesn't understand that topology. You'd need Stripe Connect with custom tax logic, which quickly becomes a custom build anyway.
Third, tax-exempt customers (non-profits, resellers, government entities) require manual handling. You can set customer.tax_exempt to 'exempt' or 'reverse' for B2B reverse-charge scenarios, but validating exemption certificates is entirely on you. Stripe doesn't verify that a customer claiming exemption is actually exempt.
// Marking a customer as tax exempt (e.g., after you've verified
// their exemption certificate offline)
await stripe.customers.update(customerId, {
tax_exempt: 'exempt', // 'none' | 'exempt' | 'reverse'
});
// For EU B2B with valid VAT number (Stripe validates the VAT number
// via VIES and applies reverse charge automatically if valid)
await stripe.customers.createTaxId(customerId, {
type: 'eu_vat',
value: 'DE123456789',
});
// After creating the tax ID, Stripe validates it.
// If valid, it sets the customer to reverse-charge automatically.The EU VAT number validation via VIES is actually one of the slicker features. B2B customers in Europe don't pay VAT when they provide a valid VAT number — this is called reverse charge, where the buyer accounts for the VAT instead of the seller. Stripe handles this correctly once you capture the VAT number. We add a VAT number field to our billing settings pages specifically for this.
Testing Tax Without Charging Real Customers
Stripe's test mode has automatic tax enabled, but it uses simplified test calculations. Don't rely on test mode numbers being exactly right — they're designed to let you test your integration, not verify your tax rates.
To test specific scenarios, you can use Stripe's test clock feature to simulate subscription renewals and see how tax is applied on recurring invoices. For checkout testing, use test addresses — a German address will trigger EU VAT calculation, a California address will trigger US sales tax if you're registered there.
Always test with addresses from jurisdictions where you're registered. Stripe Tax only calculates tax where you've added a registration in the dashboard — if you add a California address but haven't added a California registration, you'll see $0 tax and think everything's working fine. It's not.
This burned us once. We were testing and saw no tax being collected for a US address, assumed automatic tax was working correctly, and shipped. Turned out we hadn't added any US state registrations in the Stripe Tax dashboard. The integration was technically correct but completely inert. Add your registrations in the dashboard before you assume the code is wrong.
Putting It Together in a Real Billing Flow
Here's the rough checklist for adding Stripe Tax to an existing billing setup:
- Go to Stripe Dashboard > Tax > Registrations. Add every jurisdiction where you're registered (or need to be).
- Update every Product in Stripe to have the correct tax code. For SaaS, txcd_10103001 is your starting point.
- Update every Price to have tax_behavior set. 'exclusive' for US, 'inclusive' for EU/UK are sensible defaults.
- Add automatic_tax: { enabled: true } to all Checkout Sessions.
- Add automatic_tax: { enabled: true } to all Subscriptions (including existing ones you want to migrate).
- Set billing_address_collection: 'required' on Checkout Sessions so Stripe can actually calculate where the customer is.
- Add a VAT number input to your billing settings page for B2B customers.
- Handle the tax_exempt flag for customers who provide valid exemption certificates.
If you're starting from scratch with a Next.js app, our templates at peal.dev include a complete Stripe billing setup with automatic tax already wired up — checkout sessions, subscription management, webhook handlers, and the billing portal. It saves a few hours of plumbing if you're building this for the fifth time and would rather just ship.
Tax is genuinely one of those things where the code is the easy part. The annoying part is understanding when you need to register, actually registering, and making sure your accountant gets the right reports at year end. Stripe Tax handles the calculation and collection reliably — just don't expect it to replace an accountant who knows international tax law. At a certain revenue level, hiring one is genuinely the right call. Until then, Stripe Tax buys you time to focus on actually building the product.
