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.
| Engine | Who decides the tax | When to use it |
|---|---|---|
| Your tax rates | The 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
countryandstateare the customer'scountryandregion— the jurisdiction your tax rates match on. There is one jurisdiction per customer, never two that disagree. - Tax ID
typeis Stripe's tax ID type:eu_vat,gb_vat,ch_vat,au_abn,ca_gst_hst,in_gst,us_einand the rest. The number's format is checked per type before anything is stored, and it is stored compacted —de 123.456 789becomesDE123456789. An EU VAT number may be sent without its prefix if you givecountry; Greek numbers use VIES'sEL. - Add and remove tax IDs one at a time with
POSTandDELETE /v1/customers/{id}/tax-ids, or replace the list by sendingtaxIdsonPATCH /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 withbilled_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:
| Status | Meaning |
|---|---|
VERIFIED | VIES confirmed the registration. The registered name, address and consultation number are kept as evidence. |
INVALID | VIES says no such registration exists. The ID is kept and marked, and is not sent to Stripe Tax. |
UNAVAILABLE | VIES or the member state's registry could not be reached. Base316 asks again, backing off from 15 minutes to a day. |
UNVERIFIED | Well 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
VERIFIEDstays 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:
- 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.
- Each line carries a product tax code: the plan's own for its recurring
fee, the feature's for its usage (
taxCodeonPATCH /v1/plans/{id}andPATCH /v1/features/{id}, e.g.txcd_10103001for business SaaS). A line with none uses your Stripe account's preset tax code. - The customer's billing address and tax IDs (except
INVALIDones) 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". - 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}/finalizeas a503. - 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_unavailableon the/meroute) 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.
Social sign-in
Letting your users sign in with Google, Microsoft, Discord and nine others — registering each app, the callback URL, which accounts are joined by email, and restricting Microsoft to your customers' directories.
Text messages and phone numbers
Signing in with a code sent by SMS, proving a phone number, and SMS as a second factor — sent from Base316's number or your own Twilio account.

