Stripe Tax handles VAT rate calculation and collection across 30+ countries automatically. What it does not do is check whether a customer's VAT number is real, currently registered, and belongs to the company that claims it. That gap creates a compliance liability: a buyer can enter any plausible-looking VAT number, and Stripe will zero-rate all their invoices without complaint. This guide shows how to close that gap with real-time VAT validation and wire it into Stripe's `tax_exempt` flag correctly for SaaS subscription billing.
Warning
Applying reverse charge to an unverified VAT number creates a VAT liability that falls on your business, not the customer. If the number is invalid, you owe the full VAT amount on every zero-rated transaction. Validation is not optional.
How Stripe Tax Exemption Works
Stripe's tax exemption model is simple: set `tax_exempt: 'reverse'` on the Stripe Customer object, and Stripe Tax will apply zero VAT to all invoices for that customer and add the 'Reverse charge' annotation required by EU invoice rules. Your job is to make sure this flag is only set when the VAT number has been independently validated.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function validateAndConfigureCustomer(
stripeCustomerId: string,
vatNumber: string
) {
// 1. Validate the VAT number via TaxID API
const country = vatNumber.slice(0, 2).toUpperCase();
const vat = vatNumber.replace(/\s/g, '').toUpperCase();
const res = await fetch(
`https://taxid.dev/api/v1/validate/${country}/${vat}`,
{ headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` } }
);
const data = await res.json();
if (data.status === 'active') {
// 2. VAT is valid — apply zero-rate on Stripe customer
await stripe.customers.update(stripeCustomerId, {
tax_exempt: 'reverse',
metadata: {
vat_number: vat,
vat_company_name: data.company_name ?? '',
vat_validated_at: new Date().toISOString(),
vat_request_id: data.request_id,
},
});
return { outcome: 'zero_rated', companyName: data.company_name };
}
if (data.status === 'service_unavailable') {
// 3. VIES is down — charge VAT, flag for re-validation
await stripe.customers.update(stripeCustomerId, {
metadata: {
vat_number: vat,
vat_revalidate_needed: 'true',
vat_unavailable_at: new Date().toISOString(),
},
});
return { outcome: 'vat_charged_pending_revalidation' };
}
// 4. Invalid or bad format — keep standard VAT
return { outcome: 'standard_vat', reason: data.status };
}Checkout Flow for SaaS Subscriptions
For SaaS subscriptions, validate the VAT number before creating the Stripe Checkout session or before the first subscription invoice. Here is the full checkout flow with Stripe Checkout:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createB2BCheckoutSession(
customerId: string,
vatNumber: string | null,
priceId: string
) {
if (vatNumber) {
const result = await validateAndConfigureCustomer(customerId, vatNumber);
// Log outcome for audit
console.log(`VAT validation result: ${result.outcome}`, {
customerId,
vatNumber: vatNumber.slice(0, 2) + '***',
});
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
// Stripe will apply zero-rate automatically if customer.tax_exempt is 'reverse'
automatic_tax: { enabled: true },
success_url: `${process.env.APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
});
return session.url;
}Subscription Re-validation Job
VAT registrations change. Companies deregister, reorganise, or move jurisdictions. A number valid at signup may be invalid six months later. For subscription businesses, run a monthly background job that re-checks all customers with `tax_exempt: 'reverse'`.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function revalidateAllReverseChargeCustomers() {
const customers = await stripe.customers.list({
limit: 100,
// Filter customers with tax_exempt set to 'reverse'
});
const reverseChargeCustomers = customers.data.filter(
c => c.tax_exempt === 'reverse' && c.metadata.vat_number
);
const results = [];
for (const customer of reverseChargeCustomers) {
const vatNumber = customer.metadata.vat_number;
// Add delay between requests to avoid rate limits
await new Promise(r => setTimeout(r, 200));
const res = await fetch(
`https://taxid.dev/api/v1/validate/${vatNumber.slice(0,2)}/${vatNumber}`,
{ headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` } }
);
const data = await res.json();
if (data.status === 'inactive') {
// VAT registration has lapsed — remove exemption
await stripe.customers.update(customer.id, {
tax_exempt: 'none',
metadata: {
vat_invalidated_at: new Date().toISOString(),
vat_revalidation_status: 'inactive',
},
});
results.push({ customerId: customer.id, action: 'exemption_removed' });
} else if (data.status === 'active') {
// Still valid — update the last-validated timestamp
await stripe.customers.update(customer.id, {
metadata: { vat_validated_at: new Date().toISOString() },
});
results.push({ customerId: customer.id, action: 'reconfirmed' });
}
}
return results;
}Handling VIES Outages at Checkout
Note
When VIES returns `service_unavailable`, charge standard VAT at checkout. Do not block the purchase. Store the VAT number in Stripe customer metadata with `vat_revalidate_needed: 'true'` and re-validate once VIES recovers. If the number is confirmed valid, issue a credit note for the VAT charged.
Audit Trail Requirements
EU VAT compliance requires that you can prove a VAT number was valid at the time of each zero-rated invoice. Store the TaxID `request_id` in Stripe customer metadata — it uniquely identifies which API call validated the number and when. You can reference this ID if you are ever audited by a tax authority and need to prove the validation happened.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.