Stripe Tax is excellent at calculating which VAT rate applies to a given transaction. It handles jurisdiction detection, rate changes, and the mechanics of reverse charge. What it does not do is verify whether the VAT number your customer typed actually exists in the VIES registry. A buyer can enter any plausible-looking number — even a correctly-formatted one belonging to a different company — and Stripe will apply zero-rate treatment without complaint. If that number turns out to be invalid on audit, the VAT liability falls on you, not the customer.
Warning
Stripe's documentation acknowledges this gap: 'Stripe does not verify that a Tax ID is valid. You are responsible for verifying that a Tax ID is accurate and belongs to the customer.' EU VAT Directive 2006/112/EC makes VIES verification your legal responsibility before applying zero-rate.
The Two-Step Pattern: Validate First, Then Set tax_exempt
The correct pattern has two steps executed in sequence before any invoice is issued. First, call the TaxID API to check the VAT number against the live VIES registry. Second, only if the API returns status: 'active', update the Stripe customer with tax_exempt: 'reverse'. Never call stripe.customers.update with tax_exempt: 'reverse' based solely on the customer providing a correctly-formatted VAT number string.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
/**
* Call this at B2B checkout BEFORE creating the Stripe subscription.
* Returns true if VAT was validated and zero-rate applied.
*/
export async function validateAndApplyEuVat(
stripeCustomerId: string,
countryCode: string,
vatNumber: string
): Promise<{ applied: boolean; status: string; companyName: string | null }> {
// Step 1: Validate against VIES
const res = await fetch(
`https://taxid.dev/api/v1/validate/${countryCode}/${vatNumber}`,
{
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
signal: AbortSignal.timeout(5000),
}
);
const data = await res.json();
// Step 2: Only apply zero-rate if VIES confirmed active registration
if (data.status === 'active') {
await stripe.customers.update(stripeCustomerId, {
tax_exempt: 'reverse',
metadata: {
vat_number: vatNumber,
vat_company_name: data.company_name ?? '',
vat_address: data.address ?? '',
vat_validated_at: new Date().toISOString(),
vat_request_id: data.request_id,
},
});
return { applied: true, status: 'active', companyName: data.company_name };
}
// For service_unavailable: charge standard VAT and handle separately
// For invalid/inactive: do not apply zero-rate
return { applied: false, status: data.status, companyName: null };
}How Stripe Uses the tax_exempt Flag
When you set tax_exempt: 'reverse' on a Stripe customer, Stripe Tax suppresses VAT on all invoices for that customer and automatically adds the 'Reverse charge' annotation required by EU Invoice Directive Article 226. Critically, Stripe respects this flag regardless of how it was set — there is no validation step on Stripe's side. The flag means 'I, the seller, have verified this customer is reverse-charge eligible'. The burden of that verification is entirely yours.
| tax_exempt value | Stripe behavior | When to use |
|---|---|---|
| none (default) | Standard VAT applied based on customer location | B2C customers and unverified B2B |
| exempt | No VAT applied — used for genuinely tax-exempt entities (charities, governments) | Non-standard cases only |
| reverse | Zero VAT + 'Reverse charge' annotation on invoice | Only after VIES confirms VAT number is active |
Storing the Validation Record in Stripe Metadata
Store the full validation result in Stripe customer metadata immediately after applying zero-rate. The metadata fields vat_number, vat_company_name, vat_address, vat_validated_at, and vat_request_id give your finance team a complete audit trail accessible from the Stripe dashboard without needing to cross-reference a separate database. The request_id from the TaxID API is particularly valuable — it uniquely identifies the VIES query and can be used to retrieve the original response if an auditor challenges a specific invoice.
Handling VIES Downtime Gracefully
VIES has scheduled maintenance windows and per-country outages. When the TaxID API returns status: 'service_unavailable', do not block the checkout. Instead, proceed with standard VAT (tax_exempt: 'none') and add the customer to a re-validation queue. Once VIES recovers and confirms the number, update tax_exempt to 'reverse' and issue a VAT refund via Stripe's credit note feature. This approach is more commercially sound than blocking a legitimate business customer during an EU maintenance window.
// Add to queue when VIES is unavailable at checkout
export async function queueForRevalidation(
stripeCustomerId: string,
countryCode: string,
vatNumber: string
) {
await db.insert('vat_revalidation_queue', {
stripe_customer_id: stripeCustomerId,
country_code: countryCode,
vat_number: vatNumber,
queued_at: new Date().toISOString(),
retry_after: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // retry in 1 hour
});
}
// Background job — runs every hour
export async function processRevalidationQueue() {
const pending = await db.query(
`SELECT * FROM vat_revalidation_queue WHERE retry_after < now() AND resolved = false`
);
for (const item of pending) {
const res = await fetch(
`https://taxid.dev/api/v1/validate/${item.country_code}/${item.vat_number}`,
{ headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` } }
);
const data = await res.json();
if (data.status === 'active') {
await stripe.customers.update(item.stripe_customer_id, {
tax_exempt: 'reverse',
metadata: { vat_validated_at: new Date().toISOString() },
});
await db.update('vat_revalidation_queue', { id: item.id }, { resolved: true });
// Issue credit note for any VAT already charged while queued
}
}
}Subscription Re-Validation: The Monthly Check
For subscription businesses, VAT registrations can change between billing cycles. A company that was validly registered at signup may have deregistered six months later. Schedule a monthly background job that re-validates all customers with tax_exempt: 'reverse'. When a number that was previously active returns inactive or invalid, revert tax_exempt to 'none' and notify your finance team before the next invoice cycle. Issuing a zero-rated invoice to a deregistered business creates a VAT liability on the same terms as issuing one to an unverified number.
For the complete checkout integration pattern including frontend field design, server-side endpoint code, and the SQL audit schema, see How to Integrate a VAT Number Check API into Your Checkout Flow. For a use-case-specific implementation guide for Stripe, see the Stripe EU VAT use case page.
B2B vs B2C: How Stripe Determines the Tax Treatment
Stripe Tax determines whether to charge VAT based on two factors: the customer's location (billing address) and the tax_exempt flag on the customer object. For a customer with no tax_exempt flag (or tax_exempt: 'none'), Stripe charges VAT at the applicable destination-country rate — the standard B2C treatment. For a customer with tax_exempt: 'reverse', Stripe suppresses VAT entirely and adds the reverse-charge annotation. The key insight is that Stripe does not know whether the customer is a business or a consumer — that determination is yours to make, based on whether you have validated their VAT number.
In practice this means your checkout must implement the business/consumer split explicitly. The most robust pattern: show a 'Business purchase?' toggle, and when the customer activates it, show the VAT number field. If they provide a VAT number, validate it. If valid, set tax_exempt: 'reverse'. If they do not provide a VAT number (or leave the toggle off), leave tax_exempt at the default 'none' — Stripe charges standard VAT. Never rely on the customer selecting a business billing address as a proxy for B2B — residential streets in EU countries regularly appear as business billing addresses.
One edge case worth handling explicitly: a customer who enters a VAT number from a different country than their billing address. This is legitimate — a German company with a UK billing address for a branch office, for example — but it should trigger a manual check. In automated flows, the simplest approach is to accept the VAT number if VIES confirms it active, regardless of the billing country, but flag the mismatch in your CRM for your finance team to review before the first invoice is issued.