Methodology. This tutorial synthesizes the Stripe Tax documentation as of May 2026. Tax law and pricing change — verify against stripe.com/tax for current details. Statutory thresholds, registration rules, and filing requirements vary by jurisdiction; this guide is a developer’s starting point, not legal or tax advice.

There is a moment in every SaaS’s life when the tax problem stops being a footnote and becomes a real obligation. A French customer signs up and you owe French VAT. Texas hits its economic-nexus threshold and you owe Texas state sales tax. The UK introduces a digital-services rule that catches you flat-footed. By the time you find out, you’re usually six months behind on filings and two letters into a polite-but-firm correspondence with a revenue authority.

Stripe Tax exists to make this part of the payments stack mostly automatic. It calculates the right rate at checkout, applies it correctly for digital goods, handles VAT IDs for B2B reverse charges, and keeps a transaction-level record you can hand to a filing partner at quarter-end. It does not file your returns — that’s the part founders most often miss — but it makes filing a clerical task instead of a forensic one.

This guide walks the canonical eight-step setup, shows the TypeScript that wires automatic_tax into a Stripe Checkout session, covers the gotchas around the 0.5% fee and US economic nexus, and ends with a fair comparison against the Merchant-of-Record alternatives (Lemon Squeezy, Paddle) that handle this problem in a structurally different way.

1 When does Stripe Tax pay for itself?

The bigger question before any code: should you turn Stripe Tax on at all? An honest answer matters because Stripe Tax adds a 0.5% fee on top of the standard 2.9% + $0.30 per transaction, and that fee compounds as you grow.

The pragmatic decision rule, drawn from the public guidance Stripe publishes on jurisdictional thresholds and from the patterns founders post in r/SaaS and Indie Hackers:

  • Under $1K MRR, US-only customers, single state of operation. Stripe Tax is overkill. Your obligations are likely just “collect and remit in your home state, if it taxes SaaS.” A spreadsheet and your state’s online filing portal are enough. Many states don’t tax SaaS at all (California, Florida, and others); the ones that do (New York, Texas, Washington, Pennsylvania, and a growing list) are individually manageable.
  • $1K–$5K MRR, mostly US, occasional EU customer. Borderline. The EU has effectively zero threshold for B2C digital services — one B2C sale into Germany triggers VAT obligations under OSS rules. If you’re selling self-serve into the EU at all, Stripe Tax is the path of least resistance.
  • $5K+ MRR with international customers. Stripe Tax pays for itself. The 0.5% fee on a $5K MRR account is $25/month — less than an hour of your accountant’s time, and it eliminates the per-transaction calculation problem entirely.
  • $25K+ MRR, multi-jurisdiction, B2B mix. Stripe Tax is the floor; you’ll likely also pair it with a filing partner (TaxJar, Anrok, Numeral) that handles registrations and returns. The combined cost is real but small relative to revenue and to the cost of being wrong.

The other variable is your geography mix. If 100% of your customers are in countries that don’t tax SaaS (some), Stripe Tax is wasted spend. If your customers span 20 countries, you cannot reasonably calculate VAT/GST/sales-tax by hand even at low MRR — the calculation cost dominates the fee.

The case against Stripe Tax is mostly about the 0.5% fee at scale. At $100K MRR with most revenue passing through Stripe, that’s $500/month for calculation alone. Some founders prefer Anrok or TaxJar’s flat-rate API at that point and disable Stripe’s native calculation. That trade is real, but it’s a Series-A-ish problem; for the first $0–$100K MRR, Stripe Tax is the correct default.

2 Activate Stripe Tax in the Dashboard

Activation lives in the Dashboard at Settings → Tax. The flow walks you through three things: confirming your origin address (the address that determines your home-country nexus), selecting which jurisdictions you want Stripe to monitor, and choosing the activation date.

The activation date matters because it’s the moment Stripe begins calculating tax on transactions. Set it to today if you’re launching tax collection for the first time. If you’re back-filling for a period when you should have been collecting, the activation date is a flag to your accountant that pre-activation transactions need to be handled separately — usually with a voluntary disclosure agreement in the affected jurisdictions.

Once activated, you can verify the configuration programmatically via the Tax Settings API. The canonical pattern documented in the Stripe API reference:

// scripts/verify-tax-settings.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-15'
});

async function verify() {
  const settings = await stripe.tax.settings.retrieve();

  if (settings.status !== 'active') {
    throw new Error(
      `Stripe Tax is not active: status=${settings.status}`
    );
  }

  console.log('Origin address:', settings.head_office?.address);
  console.log('Default tax behavior:', settings.defaults.tax_behavior);
  console.log('Default tax code:', settings.defaults.tax_code);
}

verify().catch((err) => {
  console.error(err);
  process.exit(1);
});

Run this once after activation to confirm the configuration is what you expect. The status field returns active when Stripe Tax is enabled, pending if you started the flow but didn’t finish, and not_collecting otherwise.

3 Add your business addresses

Your origin address is the single most important field in Stripe Tax. It’s the address Stripe uses to determine your home jurisdiction, and it’s the anchor point for all subsequent nexus calculations. Set it under Settings → Tax → Locations.

If your business has multiple physical locations — an office in California and a warehouse in Texas, for example — add each one. Each physical-presence location creates an additional nexus that triggers tax-collection obligations in that state regardless of revenue thresholds. For a fully remote, single-founder SaaS, the origin address is your registered business address (typically the LLC’s home state) and that’s the entire list.

Stripe Tax then layers economic nexus on top of physical nexus. Most US states use a threshold like “$100,000 in sales or 200 separate transactions in a calendar year” to trigger an economic-nexus obligation, but the exact numbers vary — California uses $500,000, Texas uses $500,000, and a handful of states use the older 200-transaction rule. Stripe Tax monitors these thresholds and surfaces a registration alert in the Dashboard when you cross one. Registration is still your job; the alert just tells you when.

4 Set product tax codes

Every Stripe Product has a tax code that tells Stripe Tax what kind of thing it is. The code drives the rate calculation: SaaS is taxed differently from physical goods, which is taxed differently from a digital download, which is taxed differently from a service. Pick the wrong code and you collect the wrong rate.

For most SaaS, the code is txcd_10000000 — “Software as a Service (SaaS).” Stripe defines this as “cloud-based software, accessed remotely, where the customer does not download or install the software.” That’s the canonical SaaS pattern.

Other codes in the public Stripe tax-code reference that solo founders often need:

  • txcd_10103000 — Downloadable software (different VAT treatment in some EU countries)
  • txcd_10501000 — Pre-recorded online courses
  • txcd_10401100 — E-books
  • txcd_20030000 — Web hosting
  • txcd_99999999 — General services (a fallback when nothing else fits)

You can search the full list at stripe.com/docs/tax/tax-codes. Set the tax code on the Product itself rather than per-Price — the code travels with the product across all its prices and recurring intervals.

// scripts/seed-products.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-15'
});

async function seedProduct() {
  const product = await stripe.products.create({
    name: 'Pro Plan',
    tax_code: 'txcd_10000000', // SaaS
    metadata: {
      tier: 'pro'
    }
  });

  await stripe.prices.create({
    product: product.id,
    unit_amount: 2900,
    currency: 'usd',
    recurring: { interval: 'month' },
    tax_behavior: 'exclusive' // price excludes tax; see Step 6
  });

  return product;
}

If you have existing products that pre-date Stripe Tax, update them with stripe.products.update(id, { tax_code: 'txcd_10000000' }) in a one-shot migration script. Products without a tax code default to the account-level default you set in Settings → Tax → Defaults.

5 Enable Stripe Tax in Checkout

Wiring Stripe Tax into a Checkout session is one parameter: automatic_tax: { enabled: true }. Stripe handles the rest — geolocating the customer, applying the right rate, displaying the tax line on the Checkout page, and recording the collected tax on the resulting Invoice.

// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-15'
});

export async function POST(req: Request) {
  const { priceId, userId, email } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    customer_email: email,

    // Stripe Tax: calculate and collect
    automatic_tax: { enabled: true },

    // Required when automatic_tax is enabled so Stripe
    // can geolocate the customer for tax purposes.
    customer_update: {
      address: 'auto',
      name: 'auto'
    },

    // Collect billing address for tax determination
    billing_address_collection: 'required',

    // Allow VAT ID entry for B2B reverse charges (see Step 7)
    tax_id_collection: { enabled: true },

    success_url: `${process.env.APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/pricing`,
    metadata: { user_id: userId }
  });

  return NextResponse.json({ url: session.url });
}

Three required-when-enabled parameters appear above. billing_address_collection: 'required' forces the customer to enter a billing address — Stripe needs the country and (for the US) the postal code to compute the right rate. customer_update: { address: 'auto', name: 'auto' } tells Stripe to write the entered address back onto the Customer object so future invoices can reuse it. tax_id_collection: { enabled: true } shows the optional VAT ID field; if a B2B EU customer enters a valid VAT ID, Stripe applies the reverse charge automatically (Step 7).

For the embedded Checkout pattern (where the form lives inside your own page rather than redirecting to Stripe), the integration is identical — the same parameters apply to checkout.sessions.create regardless of whether you render with <EmbeddedCheckout /> or redirect:

// app/(billing)/checkout/page.tsx
'use client';

import { loadStripe } from '@stripe/stripe-js';
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout
} from '@stripe/react-stripe-js';
import { useCallback } from 'react';

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

export default function CheckoutPage({
  priceId
}: {
  priceId: string;
}) {
  const fetchClientSecret = useCallback(async () => {
    const res = await fetch('/api/checkout/embedded', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ priceId })
    });
    const { clientSecret } = await res.json();
    return clientSecret;
  }, [priceId]);

  return (
    <EmbeddedCheckoutProvider
      stripe={stripePromise}
      options={{ fetchClientSecret }}
    >
      <EmbeddedCheckout />
    </EmbeddedCheckoutProvider>
  );
}

The server side mirrors Step 5’s code with one change: ui_mode: 'embedded' and return_url instead of success_url. The full subscription pattern, including the database side and webhook handling, is in the Stripe subscriptions with Supabase tutorial.

6 Tax-inclusive vs tax-exclusive pricing

Stripe lets you choose whether a price is tax-inclusive (the headline number already contains tax) or tax-exclusive (tax is added on top). The choice is set per Price via the tax_behavior field, with values inclusive, exclusive, or unspecified.

The convention by market:

  • US customers expect tax-exclusive pricing. The shelf price is $29; the receipt shows $29 + $2.46 tax = $31.46. Use tax_behavior: 'exclusive'.
  • EU/UK customers expect tax-inclusive pricing. The shelf price is €29; that already includes 19% German VAT (so the pre-tax price is €24.37). Use tax_behavior: 'inclusive'.
  • B2B SaaS typically uses tax-exclusive globally because business buyers reclaim VAT and want to see the pre-tax number. Use tax_behavior: 'exclusive'.

The Dashboard exposes this as a “Display tax in prices” toggle under Settings → Tax → Defaults. Setting the default catches new prices created via the Dashboard; for prices created via the API, pass tax_behavior explicitly so you don’t inherit a Dashboard setting that might change later.

Pricing strategy aside, the technical detail to remember is that mixing inclusive and exclusive prices on the same Checkout session is allowed but rarely a good idea — the Checkout page’s tax line displays differently depending on the behavior, and a mixed cart confuses customers. Pick one per market and stick to it.

7 Handle reverse charges (B2B EU customers with VAT IDs)

EU VAT has a special rule for B2B sales: when a VAT-registered business in one EU country sells to a VAT-registered business in another EU country, the seller does not charge VAT. The buyer accounts for VAT on their own return under the “reverse charge” mechanism. This is huge for B2B SaaS because it means a German company buying your tool doesn’t pay VAT to you — they handle it themselves.

Stripe Tax handles reverse charges automatically when three conditions are met: the customer enters a VAT ID at checkout, the VAT ID is valid (Stripe checks against the EU’s VIES database in real time), and the seller and buyer are in different EU countries (or the seller is outside the EU and the buyer is in the EU).

The setup is one parameter on the Checkout session, already shown in Step 5: tax_id_collection: { enabled: true }. With it enabled, the Checkout form shows a “Add VAT ID” field. When the customer fills it in with a valid ID, Stripe validates against VIES, applies reverse charge to the invoice, and writes the VAT ID onto the Customer object for future use.

You can also set tax IDs programmatically when creating customers from your own UI:

// app/api/customer/create/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-15'
});

export async function POST(req: Request) {
  const { email, name, address, vatId, vatType } = await req.json();

  const customer = await stripe.customers.create({
    email,
    name,
    address // {line1, city, postal_code, state, country}
  });

  if (vatId) {
    // vatType examples: 'eu_vat', 'gb_vat', 'au_abn', 'ca_gst_hst'
    await stripe.customers.createTaxId(customer.id, {
      type: vatType,
      value: vatId
    });
  }

  return NextResponse.json({ customerId: customer.id });
}

Listen for the customer.tax_id.created webhook event to mirror the VAT ID into your own database, since you’ll often want to display it on invoices, gate B2B-only features, or pre-fill the field on subsequent purchases:

// app/api/webhooks/stripe/route.ts (excerpt)
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { sql } from '@/lib/db';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-01-15'
});

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return NextResponse.json({ error: 'invalid signature' }, { status: 400 });
  }

  switch (event.type) {
    case 'customer.tax_id.created': {
      const taxId = event.data.object as Stripe.TaxId;
      await sql`
        UPDATE billing_customers
        SET vat_id = ${taxId.value},
            vat_type = ${taxId.type},
            vat_verified = ${taxId.verification?.status === 'verified'}
        WHERE stripe_customer_id = ${taxId.customer as string}
      `;
      break;
    }
    case 'customer.tax_id.deleted':
    case 'customer.tax_id.updated':
      // mirror similarly
      break;
  }

  return NextResponse.json({ received: true });
}

The verification.status field is the one that matters. A VAT ID can be entered but unverified (the customer typed it in but VIES rejected it), or verified against the EU registry. Reverse charge applies only to verified IDs — an unverified ID falls back to the standard B2C rate.

8 File and remit (Stripe Tax does not)

The crucial fact about Stripe Tax: it calculates and collects, but it does not file your returns or remit the collected tax to revenue authorities. Filing remains your responsibility. The path forward is one of three:

  • Connect a filing partner. TaxJar (Stripe-acquired, integrates natively), Anrok, Numeral, and a handful of regional players read your Stripe Tax transaction data via the Stripe API or a direct integration, generate the right return for each jurisdiction, and either auto-file or hand you a filing-ready PDF. Pricing is typically a monthly base ($79–$199) plus a per-filing fee. For a SaaS in 5–10 jurisdictions, expect $150–$500/month in filing-partner cost.
  • Hand the data to your accountant. Stripe Tax ships a Tax Reports dashboard that exports a per-jurisdiction summary in CSV. An accountant who has filed sales tax before can take this and file in your home jurisdictions. Cheaper at low volume, brittle at scale.
  • File yourself. Most US states have an online filing portal that accepts a quarterly or monthly return. The EU OSS scheme lets you file one quarterly return for all EU member states through your home country’s OSS portal. Tedious but possible up to 5 jurisdictions.

The combination most solo founders end up with: Stripe Tax + Anrok or TaxJar. Stripe Tax handles the per-transaction math and the VAT-ID validation; the filing partner handles registrations, returns, and the obnoxious paperwork that varies by state. Together they cost roughly 0.5% (Stripe) + ~$200/month (filing partner), which is small relative to the cost of getting a sales-tax audit wrong.

Common gotchas

The 0.5% Stripe Tax fee is on top of standard processing

Stripe’s base rate is 2.9% + $0.30 per transaction. With Stripe Tax enabled, the effective rate becomes 3.4% + $0.30. On a $29 monthly subscription, that’s $1.29 in fees instead of $1.14 — a 13% increase in payment costs. Worth running the math: at $10K MRR, Stripe Tax adds $50/month vs roughly $150/month for an accountant to do the same work manually. The break-even is low; the structural answer is “turn it on once you have any international customers.” The full base-rate breakdown is in the Stripe pricing explained guide.

US economic-nexus thresholds vary by state

The headline rule of thumb is “$100K or 200 transactions per state, per calendar year,” but the actual numbers and the “or vs and” logic differ. California is $500K, Texas is $500K, New York is $500K and 100 transactions, and several states have dropped the 200-transaction rule entirely. Stripe Tax monitors all of this and alerts you in the Dashboard when you cross a threshold; do not try to track this by hand.

EU VAT OSS registration is still your job

Stripe Tax calculates EU VAT correctly and applies reverse charge to verified B2B sales. But to actually collect EU VAT on B2C sales, you need to register for the One Stop Shop (OSS) scheme in an EU member state — either the country where your business is established, or (for non-EU sellers) the EU member state of your choice. OSS lets you file one quarterly VAT return covering all EU sales instead of registering separately in 27 countries. Stripe Tax doesn’t register you for OSS; you do that through the relevant tax authority’s portal.

Webhooks for tax events are not enabled by default

The customer.tax_id.created, customer.tax_id.updated, and customer.tax_id.deleted events are part of the standard webhook event set, but you have to subscribe to them explicitly in your webhook endpoint configuration. Add them alongside the invoice.* and customer.subscription.* events you’re already listening to.

Test mode and live mode have separate Tax configurations

Activating Stripe Tax in test mode does not activate it in live mode, and vice versa. Configure both. The product tax codes and tax behaviors are also per-mode, so a product seeded in test mode with the right code will need re-seeding in live mode.

Lemon Squeezy and Paddle: the Merchant-of-Record alternative

Stripe Tax solves the calculation problem. It does not change the fact that you are the seller of record — the entity legally responsible for collecting, filing, and remitting tax in every jurisdiction where you have nexus. That responsibility is non-trivial.

Lemon Squeezy, Paddle, and (to a lesser extent) FastSpring offer a structurally different deal: they act as the Merchant of Record (MoR), meaning they are the legal seller, they hold nexus in every jurisdiction, and they file and remit. You sell to them at a wholesale rate; they sell to your customer at retail. Tax becomes their problem entirely.

The trade-off:

  • Lemon Squeezy / Paddle: ~5% + $0.50 per transaction. No tax setup, no nexus monitoring, no filings ever. You hand over the customer relationship to a degree — the buyer’s receipt comes from Lemon Squeezy, refunds and chargebacks flow through them.
  • Stripe + Stripe Tax + filing partner: ~3.4% + $0.30 + ~$200/month flat. You retain full control of the customer relationship and can negotiate Stripe’s rates down at scale. Filing remains your operational responsibility through the partner.

For most solo founders launching their first paid product, Lemon Squeezy is the lower-friction choice. For founders building a B2B SaaS where customers expect to deal with you directly (procurement, invoicing, custom terms), Stripe + Stripe Tax is the right answer. The full feature comparison is in the Lemon Squeezy vs Stripe breakdown; the Merchant-of-Record concept itself is explained at length in what is a Merchant of Record; the broader payment-processor decision is covered in best payment processor for SaaS.

If your SaaS is a marketplace where one user pays another (think Substack, Gumroad-for-services, or anything involving payouts to creators), Stripe Connect is the underlying primitive and tax becomes more complicated — the platform must decide whether it’s the seller or the marketplace, and Stripe Tax behaves differently in each case. The Stripe Connect tutorial covers the marketplace path.

Summary
Activate → addresses → codes → automatic_tax → reverse charge → filing partner

Stripe Tax turns the most painful part of running a global SaaS into a configuration problem instead of a research problem. The eight steps above are the canonical pattern from the Stripe documentation. The 0.5% fee is the price of not thinking about VAT, and for almost every founder past the first international customer, that’s a trade worth taking. Pair Stripe Tax with a filing partner (TaxJar, Anrok, Numeral) for the part Stripe explicitly does not handle. If you don’t want to deal with any of this, ship on Lemon Squeezy or Paddle and let them be the Merchant of Record.

Related guides

Get one SaaS build breakdown every week

The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.