Skip to content
ExponaBLOG
PAYMENT ENGINEERING & STRIPE INTEGRATION · DAY 039 / 100

Taxes in online payments: a hands-on guide for engineers

Collect the right tax.

Expona Admin8 min read

Why engineers need to understand tax compliance

Tax calculation in payment systems looks simple on the surface: multiply the order total by the tax rate. In practice, it's one of the most complex and legally fraught areas of payment engineering. Taxes in online payments: a hands-on guide for engineers will walk you through the real mechanics—how to collect the right information, integrate with Stripe Tax, display amounts correctly on invoices, and understand when you need to register for tax in different jurisdictions. We'll focus on the practical side: what your code needs to do, when it needs to do it, and where mistakes create problems that compound over months.

Most teams discover tax complexity the hard way. You ship checkout, process thousands of orders, then discover your tax calculations were wrong for three states. Or you realize you've been collecting sales tax in jurisdictions where you had no obligation to collect it. As your payment volume scales, tax accuracy becomes a compliance issue that can result in audits, fines, and re-filing.

Stripe Tax calculates rates

Stripe Tax is the most practical starting point for most teams. Rather than building your own lookup tables and recalculating whenever a jurisdiction changes its rates, Stripe maintains the tax rate database and exposes it through their API. When you create a checkout session or calculate tax on an order, Stripe determines the applicable rate based on the customer's location and your product category.

Here's how the flow works in practice:

  1. You have a customer location (state, province, country).
  2. You know your business location (where you're based or where you have nexus).
  3. You know your product type (physical goods, digital service, license, etc.).
  4. Stripe Tax queries its database and returns the applicable rate.

To use Stripe Tax, start by enabling it in your Stripe dashboard and configuring your business locations and nexus. Then, when creating a checkout session, pass the customer address and request tax calculation:

typescript
const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card'],
  line_items: [
    {
      price_data: {
        currency: 'usd',
        product_data: {
          name: 'Widget',
          tax_code: 'txcd_10000000', // Physical good
        },
        unit_amount: 5000, // $50.00
      },
      quantity: 1,
    },
  ],
  customer_email: 'customer@example.com',
  billing_address_collection: 'required',
  shipping_address_collection: {
    allowed_countries: ['US', 'CA', 'GB'],
  },
  automatic_tax: {
    enabled: true,
  },
  mode: 'payment',
  success_url: 'https://example.com/success',
  cancel_url: 'https://example.com/cancel',
});

The key here is automatic_tax: { enabled: true }. With this flag, Stripe calculates tax based on the billing or shipping address the customer provides during checkout. The response includes the tax amount in the session details.

One important detail: Stripe Tax codes. You assign a tax code to each product based on what it is. Physical goods, digital services, licenses, and subscriptions have different codes. You can see the full list in the Stripe documentation. If you get the code wrong, the rate calculation can be significantly off. For example, software licenses might be tax-exempt in some jurisdictions, but if you code them as physical goods, you'll collect tax incorrectly.

Collect addresses for tax

Tax depends entirely on location. You can't calculate the right rate without knowing where the customer is. That means your checkout flow must collect and validate the customer's address before calculating tax.

In a Stripe Checkout session, you control address collection with billing_address_collection and shipping_address_collection. Set one to 'required' to force the customer to enter it:

typescript
const session = await stripe.checkout.sessions.create({
  line_items: [...],
  billing_address_collection: 'required',
  automatic_tax: {
    enabled: true,
  },
  // ... rest of session
});

If you're building a custom checkout (not using Stripe Checkout), you need to collect the address yourself and pass it to the tax calculation endpoint. Stripe provides the tax.calculations.create API for this:

typescript
const calculation = await stripe.tax.calculations.create({
  currency: 'usd',
  customer_details: {
    address: {
      line1: '123 Main St',
      line2: null,
      city: 'San Francisco',
      state: 'CA',
      postal_code: '94103',
      country: 'US',
    },
    ip_address: req.ip, // Optional but helpful for validation
  },
  line_items: [
    {
      amount: 5000, // $50.00 in cents
      reference: 'product_123',
      tax_code: 'txcd_10000000',
    },
  ],
});

The response includes tax_amount_exclusive and tax_amount_inclusive, plus a breakdown by jurisdiction. Store the tax_calculation_id because you'll need it later when creating the charge or invoice.

A practical note: address collection is where many teams run into friction. Customers dislike providing their full address at checkout. In many payment flows, you don't strictly need shipping details until fulfillment. The compromise many teams use is to make billing address required (for tax) but shipping address optional, collecting it only if the customer purchases a physical product.

Show tax on invoices

Once you've calculated tax, you need to display it clearly to the customer and yourself. That means showing it on the invoice, confirmation email, and any receipt page.

If you're using Stripe Invoices, you can link your tax calculation to the invoice when you create it:

typescript
const invoice = await stripe.invoices.create({
  customer: customer_id,
  currency: 'usd',
  auto_advance: false,
  custom_fields: [
    {
      name: 'Order ID',
      value: 'ORD-12345',
    },
  ],
});

Then add line items:

typescript
await stripe.invoiceItems.create({
  customer: customer_id,
  amount: 5000,
  currency: 'usd',
  description: 'Widget',
  invoice: invoice.id,
  tax_code: 'txcd_10000000',
});

When you finalize the invoice, Stripe automatically calculates and applies tax:

typescript
const finalized = await stripe.invoices.finalizeInvoice(invoice.id);

The finalized invoice includes the tax breakdown. If you're building your own invoicing system, you'll need to render the tax calculation yourself. Store the tax amount with the order in your database and include it in your invoice template:

typescript
// In your order creation
const order = {
  customer_id,
  subtotal: 5000,
  tax_amount: 412, // From Stripe Tax calculation
  tax_rate: 0.0825,
  total: 5412,
  tax_calculation_id, // Reference to Stripe's calculation
  created_at: new Date(),
};

On your invoice template (HTML, PDF, etc.), break down the total clearly:

  • Subtotal: $50.00
  • Tax (CA): $4.12
  • Total: $54.12

This clarity is not just good UX—it's a legal requirement in many jurisdictions. Customers need to see what tax they're paying and to which jurisdiction it applies.

Register where required

Calculating and collecting tax is only half the story. You also need to remit it to the relevant tax authorities. Whether you're required to collect and remit tax depends on your nexus—your connection to a jurisdiction.

Nexus rules vary significantly by country. In the United States, you have sales tax nexus in a state if you have a physical presence (office, warehouse, employee) or, in many states, if you exceed a sales threshold (often $100,000–$500,000 in annual sales). When you have nexus, you're required to collect and remit sales tax on transactions in that state.

The practical step: go to your Stripe account settings and declare your nexus locations. This tells Stripe which tax rates to apply and where you're actually required to collect. If you declare nexus in California but not in Texas, Stripe will collect tax on California orders but not Texas orders.

Many teams use tax software like TaxJar or Avalara to track their nexus and filing obligations. These services integrate with Stripe and pull transaction data, then help you file returns with the right jurisdictions. If you're a small team with low transaction volume, manual tracking is feasible. Once you hit scale, you need automation.

An important detail: just because you calculate and collect tax doesn't mean you've remitted it. You still need to file tax returns and send the collected money to the authorities on schedule. Stripe doesn't do this for you—they calculate the rate and collect the money. You're responsible for the filing and remittance.

Common mistakes

Not setting tax codes correctly. Assign the wrong tax code to a product and you'll calculate the wrong rate across all transactions. Audit your product catalog against Stripe's tax code list. Digital products especially—they're often tax-exempt, but only if you code them correctly.

Collecting tax without declaring nexus. You can't just decide to collect tax everywhere. You're only legally required to collect in jurisdictions where you have nexus. If you collect in a jurisdiction where you have no nexus, you've collected money you have no right to keep or remit. Declare your nexus locations in Stripe and stick to them.

Forgetting to revalidate addresses. If a customer provides an address at checkout but their billing card is from a different state, which address do you use for tax? The billing address (card location) or the shipping address? Make a decision and stick with it. Most teams use the shipping address for physical goods and the billing address for digital services.

Not storing the tax calculation ID. Store tax_calculation_id with every order. If there's ever a dispute or audit, you need to prove what Stripe calculated and when. Without this reference, you're vulnerable.

Ignoring subscription and recurring payment tax. If you charge subscriptions, you need to recalculate tax on each billing cycle (address may have changed). Don't just lock in the first month's tax rate.

Key takeaways

  • Stripe Tax handles rate lookups; enable it with automatic_tax: { enabled: true } and assign correct tax codes to products.
  • Collect customer address at checkout with billing_address_collection: 'required' to calculate the right rate.
  • Display tax clearly on invoices, including the jurisdiction and amount, for transparency and compliance.
  • Declare nexus locations in Stripe and only collect tax where you're legally required to do so.
  • Store tax_calculation_id with every order and use tax software to track remittance obligations as you scale.
  • Use Stripe's tax.calculations.create API for custom checkout flows to keep calculations consistent.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles