Base 3:16
Guides

Tax, VAT numbers and Stripe Tax

The two tax engines — your own rates, or Stripe Tax on your connected account — customer billing addresses and tax IDs, and how EU VAT numbers are checked with VIES.

Every invoice Base316 issues is taxed by one of two engines, chosen per environment. Either way you stay the merchant of record: the tax is collected for you, on your registrations, and you file and remit it.

EngineWho decides the taxWhen to use it
Your tax ratesThe rates you define (Settings → Tax), matched on the customer's country and region.One or a few jurisdictions you know well; no Stripe account needed.
Stripe Tax (opt-in)Stripe Tax, on your connected Stripe account, per invoice and per line.Selling into many places; EU VAT with reverse charge; US sales tax by postal code.

Switch with Settings → Tax → Calculate tax with Stripe Tax. It needs a connected Stripe account that can take charges. Turning it off returns the environment to its tax rates from the next invoice; invoices already issued keep the tax they were issued with, and each says which engine taxed it.

Billing addresses and tax IDs

A customer carries a billing address and any number of tax registrations.

const customer = await base316.customers.create({
	externalId: "acct_42",
	name: "Beispiel GmbH",
	address: {
		line1: "Hauptstr. 1",
		city: "Berlin",
		postalCode: "10115",
		country: "DE",
	},
	taxIds: [{ type: "eu_vat", value: "DE123456789" }],
});
  • The address's country and state are the customer's country and region — the jurisdiction your tax rates match on. There is one jurisdiction per customer, never two that disagree.
  • Tax ID type is Stripe's tax ID type: eu_vat, gb_vat, ch_vat, au_abn, ca_gst_hst, in_gst, us_ein and the rest. The number's format is checked per type before anything is stored, and it is stored compacted — de 123.456 789 becomes DE123456789. An EU VAT number may be sent without its prefix if you give country; Greek numbers use VIES's EL.
  • Add and remove tax IDs one at a time with POST and DELETE /v1/customers/{id}/tax-ids, or replace the list by sending taxIds on PATCH /v1/customers/{id}. Up to 10 per customer.
  • Your end users can set their own: in the hosted billing portal's Billing details form, through GET/PATCH /v1/me/billing-details (publishable key plus their session), in <BillingPortal />, or by dropping in <BillingDetailsForm /> from @base316/react. A managed tenant's members are refused with billed_by_parent — the firm's account is the one invoiced.
  • Invoices print the address and tax IDs as they were when the invoice was issued; a later change does not rewrite an issued document.

VIES: checking EU VAT numbers

An EU VAT number is checked against the European Commission's VIES service when it is added, and its result is the tax ID's status:

StatusMeaning
VERIFIEDVIES confirmed the registration. The registered name, address and consultation number are kept as evidence.
INVALIDVIES says no such registration exists. The ID is kept and marked, and is not sent to Stripe Tax.
UNAVAILABLEVIES or the member state's registry could not be reached. Base316 asks again, backing off from 15 minutes to a day.
UNVERIFIEDWell formed, and there is no registry Base316 can ask — every non-EU type.

Two rules matter more than the table:

  • VIES never holds up an invoice. Member-state registries go offline most nights. A number VIES cannot answer for is still sent to Stripe Tax, and the invoice is issued; the check is retried in the background.
  • Confirmed numbers are re-checked monthly, because a registration can be cancelled. A number already VERIFIED stays verified through an outage.

The dashboard's customer page shows each ID's status, what VIES returned, when it was checked and when it will be checked next, with a button to check again.

Stripe Tax

With Stripe Tax on, every invoice finalization — and every upcoming-invoice preview — asks Stripe Tax what the invoice owes:

  1. The invoice's taxable amount (after discounts and credits) is split across its lines in minor units, so Stripe sees exactly the figure the invoice states.
  2. Each line carries a product tax code: the plan's own for its recurring fee, the feature's for its usage (taxCode on PATCH /v1/plans/{id} and PATCH /v1/features/{id}, e.g. txcd_10103001 for business SaaS). A line with none uses your Stripe account's preset tax code.
  3. The customer's billing address and tax IDs (except INVALID ones) go with it. Whether a cross-border EU sale to a business is reverse charged is Stripe's decision; when it is, the invoice and its PDF say "Reverse charge".
  4. The calculation is recorded on the invoice, and once the invoice is issued Base316 records a Tax Transaction from it, so Stripe's tax reports and filings include the sale. Voiding the invoice reverses the transaction; a credit note reverses its total as it is issued — the same amount whether it refunds the card, credits the wallet or reduces what is due.

The preview and the bill ask the same question in the same order, so the tax on the upcoming invoice is the tax on the invoice the billing run raises.

What to set up in Stripe

Stripe Tax runs on your connected account, under your registrations:

  • Turn on Stripe Tax in your Stripe dashboard and set your origin address and preset tax code.
  • Add a registration for every jurisdiction you are registered to collect in (Tax → Registrations). Where you have none, Stripe Tax returns zero with the reason not_collecting, and the invoice line says so — that is not an error, it is what you told Stripe.
  • Base316 sends prices as tax-exclusive: tax is added on top.

When Stripe Tax cannot answer

An invoice is never issued with zero tax because Stripe Tax was unreachable.

  • At finalization, a failure — Stripe unreachable, or refusing the customer's address — leaves the invoice a draft, with the reason shown on the invoice page and returned by POST /v1/invoices/{id}/finalize as a 503.
  • Base316 finalizes it again on its own, from five minutes later and backing off to every six hours, with the same options the first attempt used. Once Stripe answers it is issued and collected as normal.
  • A customer with no billing country is not sent to Stripe at all: the draft waits for an address, and the customer page warns about it. In the US and Canada Stripe also needs a postal code.
  • The upcoming-invoice preview returns 503 (tax_unavailable on the /me route) rather than a figure without its tax.
  • If the transaction cannot be recorded right after issuing, the invoice is still issued — the tax is on it — and the transaction is recorded within minutes by a background sync. A void is reversed by the same sync; a credit note's reversal is made as it is issued, and by the sync if Stripe could not be reached then.

What is checked against Stripe, and what only against a fake

Base316's own test suite runs every part of this against fakes of the VIES and Stripe Tax HTTP APIs — calculation, transaction, reversal, the unreachable and refused cases, and preview-equals-invoice. The rates Stripe applies to your registrations, the reverse-charge decision itself and the reports only exist in your Stripe account: try a few invoices in your sandbox (a Stripe test account with test-mode registrations) before turning it on in production.

On this page