---
title: "Enable Stripe Tax for Your Monetized API"
description: "Cross-border VAT and sales tax ship audits, not features. Stripe Tax inside Zuplo's billing profile gives you the right rates by customer location and invoice tax lines from one config change, not a separate engine."
canonicalUrl: "https://zuplo.com/blog/2026/04/29/stripe-tax-for-your-monetized-api"
pageType: "blog"
date: "2026-04-29"
authors: "martyn"
tags: "API Monetization"
image: "https://zuplo.com/og?text=Enable%20Stripe%20Tax%20for%20Your%20Monetized%20API"
---
Your first paying customer in the UK signs up on a Tuesday. That afternoon, you
owe HMRC. Not next quarter, not "once you cross a threshold", that afternoon,
because the UK has charged VAT on digital services from the first pound
since 2015. Germany works the same way for any EU customer. Most US states
differ in shape but not in spirit: cross a state's economic nexus threshold
(sounds more sci-fi than it is, really just a sales-volume or transaction-count
line that triggers a tax obligation) and they expect collection retroactive to
the day you crossed.

Tax compliance doesn't ship features, doesn't move retention, and does ship
audits. Most API teams ignore it until an accountant panics, or they bolt on a
separate tax engine and a webhook to keep prices in sync.

Zuplo's monetization stack uses Stripe Tax directly from the billing profile, so
the same subscription that bills the customer also calculates, collects, and
lines up the VAT or sales tax for remittance. No second engine, no sync job, no
separate invoicing pipeline.

<CalloutAudience
  variant="useIf"
  items={[
    `You're charging for API access through Zuplo's monetization and Stripe`,
    `Your first non-domestic customer is about to sign up, or has already`,
    `Your finance team has asked where the VAT line on the invoice is going to come from`,
  ]}
/>

## Why API tax is the work nobody wants

Tax on a digital subscription is a multi-axis problem.

**Where you have to register.** UK and EU charge VAT from the first sale to a
local consumer. The EU's One-Stop-Shop (OSS) lets you remit for all 27 from one
return, but you still have to register somewhere. US economic nexus thresholds
vary by state (commonly $100k in sales or 200 transactions a year), and crossing
one means collecting from day one, often retroactively.

**Who you're charging.** A B2C sale to a French consumer takes French VAT. A B2B
sale to a French business with a valid VAT number takes the reverse-charge
mechanism instead: zero VAT charged, the buyer accounts for it on their own
return, and your invoice has to state that the reverse charge applies.

**How prices are quoted.** UK and EU custom is tax-inclusive ($9.99 _includes_
VAT). US custom is tax-exclusive ($9.99 _plus_ tax). The same plan needs both
behaviours depending on where the customer logs in from.

**What category your product is.** Stripe Tax categorises every product, and the
wrong code calculates the wrong rate. Stripe publishes the full list at
[docs.stripe.com/tax/tax-codes](https://docs.stripe.com/tax/tax-codes).

The bolt-on path is a separate tax engine (Avalara, TaxJar, Stripe Tax called
from your own service) plus a sync job to keep prices, registrations, and
invoice line items aligned. Three systems with three deploy pipelines for a
feature that returns no signups.

## What Stripe Tax does

Stripe Tax sits inside Stripe and handles the parts that don't change per
customer:

- Tracks your nexus and registrations across countries and US states.
- Resolves the customer's tax location from address, IP, and payment method.
- Calculates the correct rate at invoice time using the product's tax code.
- Returns line items the invoice can render in the customer's expected format
  (inclusive or exclusive).
- Produces the reports you need to file and remit.

It doesn't decide _whether_ you should be registered somewhere. That's your
accountant's call. Once the registration exists, calculation, collection, and
reporting flow through every invoice without you wiring it up per plan.

## Enable Stripe Tax in your billing profile

Zuplo exposes monetization config through its metering API. Your billing profile
is the object on that API that holds plan billing, invoicing, and tax settings.
Tax collection is a flag on that profile, plus your supplier country. One PUT
once your Stripe registrations are in place, and the next invoice carries the
right tax line.

### Prerequisites

This post assumes you already have a monetized API running in Zuplo. If you
don't, the
[monetization quickstart](https://zuplo.com/docs/articles/monetization/quickstart)
covers that setup; come back here once your billing profile exists.

Add tax registrations in Stripe for every country where you've registered to
collect. Stripe Tax can't calculate for a jurisdiction it doesn't know you're
registered in. Registrations live at
[dashboard.stripe.com/tax/registrations](https://dashboard.stripe.com/tax/registrations),
keyed by your local registration number.

You'll also need two values from your Zuplo project to call the metering API:

- `ZUPLO_BUCKET_ID`, your monetization bucket. Find it under **Services →
  Monetization Service → Bucket Details** in the Zuplo portal. It looks like
  `bckt_g1YMyT8DM90S0iZNmVMJiFohe0sWGhxF`.
- `ZUPLO_API_KEY`, created in project settings. Treat it like any other
  server-side secret.

![Zuplo project Monetization Service panel with the Bucket Details popover open, showing the Bucket ID prefixed with bckt_](/blog-images/stripe-tax-for-your-monetized-api/bucket-id.png)

### Fetch your billing profile

The metering API expects the entire billing profile back on PUT, not a partial
diff. Two GETs: one to find the ID, one to write the full body to a file you'll
edit.

```bash
curl -X GET "https://dev.zuplo.com/v3/metering/${ZUPLO_BUCKET_ID}/billing/profiles" \
  -H "Authorization: Bearer ${ZUPLO_API_KEY}" \
  -H "Content-Type: application/json"
```

The response has an `items` array. Save the `id` as `BILLING_PROFILE_ID`, then:

```bash
curl -X GET "https://dev.zuplo.com/v3/metering/${ZUPLO_BUCKET_ID}/billing/profiles/${BILLING_PROFILE_ID}" \
  -H "Authorization: Bearer ${ZUPLO_API_KEY}" \
  -H "Content-Type: application/json" \
  > billing-profile.json
```

### Edit the tax fields

Open `billing-profile.json` and update three parts. The supplier country is the
one you're collecting tax _from_ (your company's country, ISO 3166-1 alpha-2).
`workflow.tax` is the on-switch. `invoicing.defaultTaxConfig` sets the tax
behaviour and product tax code. Leave every other field from the GET response
exactly as-is:

```json
{
  "supplier": {
    "name": "Stripe Account",
    "addresses": [
      {
        "country": "GB"
      }
    ]
  },
  "workflow": {
    "tax": {
      "enabled": true,
      "enforced": false
    }
  },
  "invoicing": {
    "autoAdvance": true,
    "draftPeriod": "P0D",
    "dueAfter": "P0D",
    "progressiveBilling": true,
    "defaultTaxConfig": {
      "behavior": "exclusive",
      "stripe": {
        "code": "txcd_10000000"
      }
    }
  }
}
```

`txcd_10000000` is Stripe's "General - Electronically Supplied Services" code,
the broadest sensible default for a metered API. `enforced: false` puts you in
best-effort mode, which is the right starting choice (more on that below).
`behavior: "exclusive"` adds tax on top of the listed price ($9.99 plan + 20%
VAT charges $11.99); switch to `"inclusive"` if your pricing page already shows
tax-included prices, which is the European default.

<CalloutTip variant="tip">
  Zuplo's tax-collection doc uses `txcd_10000000` in its example and
  shorthand-labels it "SaaS", but Stripe's own catalog reserves the SaaS label
  for `txcd_10103000` (personal use) and `txcd_10103001` (business use). Swap in
  `txcd_10103001` if your product is closer to a flat-rate business SaaS
  subscription than a pay-per-call API.
</CalloutTip>

### Save the changes

Send the entire `billing-profile.json` back to the same URL:

```bash
curl -X PUT "https://dev.zuplo.com/v3/metering/${ZUPLO_BUCKET_ID}/billing/profiles/${BILLING_PROFILE_ID}" \
  -H "Authorization: Bearer ${ZUPLO_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @billing-profile.json
```

The next invoice for a customer in a country you're registered in will carry the
right tax line.

## Best-effort first, strict later

The `enforced` flag controls what Stripe does when tax calculation fails.
Calculation can fail for a handful of predictable reasons: a customer in a
country where you don't yet have a Stripe Tax registration, a missing or invalid
postal code, or an address Stripe Tax can't map to a known jurisdiction.

The flag decides what happens next. Either those failures become a silent gap on
the invoice, or they become a hard error that blocks the subscription from being
created.

| `enabled` | `enforced` | Behaviour                                                                                                 |
| --------- | ---------- | --------------------------------------------------------------------------------------------------------- |
| `false`   | `false`    | No tax calculation. Invoices are created without tax lines.                                               |
| `true`    | `false`    | Best-effort. Tax is calculated where possible. If the customer's country has no registration, no tax.     |
| `true`    | `true`     | Strict. If Stripe Tax errors (no registration, invalid address), the invoice fails and signup is blocked. |

Strict mode sounds like the safe default. It's the loudest way to break signup.
A French startup signs up, you don't have an FR registration yet, the invoice
errors, they bounce. Best-effort lets the signup land, the invoice goes out
without tax, and you fix the gap by adding the registration. Once you've
registered everywhere your customers actually live, flip `enforced` to `true` so
a missing registration becomes a noisy error instead of a silent
under-collection.

<CalloutTip variant="mistake">
  Turning on `enforced: true` before adding registrations for every country your
  customers can sign up from. The first invoice from an unregistered country
  fails, the customer can't subscribe, and you've blocked your own funnel. Stay
  in best-effort mode until your Stripe registrations cover every country a real
  customer might sign up from, then promote to strict once that map is in place.
</CalloutTip>

## What the customer sees

The same `defaultTaxConfig` produces the right invoice for each jurisdiction:

- **UK consumer:** listed price plus a VAT line at 20%, calculated from their
  billing address.
- **French business with a verified VAT number:** listed price plus a "VAT
  reverse charge" line at zero. Stripe Tax recognises B2B intra-EU sales and
  applies the reverse-charge mechanism without per-customer wiring.
- **US customer in a state where you have nexus:** listed price plus state and
  local sales tax.
- **US customer in a state where you don't:** listed price, no tax line.

Each invoice carries the right line, and you don't write any of it.

## When to enable tax collection

The trigger is the first paying customer outside your home jurisdiction. Once
they sign up, the clock starts on the local rule and the only thing that gets
harder by waiting is the back-tax bill. Turn `enabled: true` with
`enforced: false` the same week you publish your first paid plan, then flip to
`enforced: true` once your registration map catches up with your customer map.

None of this work moves a metric your team cares about. Doing it inside the
gateway you've already wired up for billing is the difference between shipping a
feature and standing up a second system to keep the first one compliant.

<CalloutDoc
  title="Tax Collection Reference"
  description="Full reference for enabling Stripe Tax on a Zuplo billing profile, the enforcement modes, tax behaviour config, and the troubleshooting paths for missing tax lines."
  href="https://zuplo.com/docs/articles/monetization/tax-collection"
  icon="book"
/>