E-commerce platforms serving EU businesses must validate VAT numbers at checkout to legally apply zero-rate B2B exemptions. Unlike SaaS platforms that can re-validate on each invoice, e-commerce checkouts are one-shot transactions — the validation happens at the moment of purchase and the result determines the tax treatment on that specific order. Getting the checkout flow right the first time is therefore more critical than in a subscription billing context.
B2B vs B2C Detection at Checkout
The first decision in any EU e-commerce checkout is whether the buyer is B2B or B2C. The simplest signal: is a VAT number provided? If yes, the buyer is claiming B2B status and you must validate the number. If no, treat as B2C and charge local VAT at the standard rate for the buyer's country.
interface CheckoutCustomer {
country: string;
vatNumber?: string;
}
type TaxTreatment =
| { type: 'b2c'; vatRate: number }
| { type: 'b2b_exempt'; vatNumber: string; companyName: string | null }
| { type: 'b2b_pending'; vatNumber: string; reason: 'unavailable' };
async function determineTaxTreatment(
customer: CheckoutCustomer,
vatRates: Record<string, number>
): Promise<TaxTreatment> {
if (!customer.vatNumber) {
return { type: 'b2c', vatRate: vatRates[customer.country] ?? 0.20 };
}
const vat = customer.vatNumber.replace(/\s/g, '').toUpperCase();
const country = vat.slice(0, 2);
const res = await fetch(`https://taxid.dev/api/v1/validate/${country}/${vat}`, {
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
signal: AbortSignal.timeout(5000),
});
const data = await res.json();
if (data.status === 'active') {
return { type: 'b2b_exempt', vatNumber: vat, companyName: data.company_name };
}
if (data.status === 'service_unavailable') {
// VIES down — charge VAT, flag for review
return { type: 'b2b_pending', vatNumber: vat, reason: 'unavailable' };
}
// inactive or format_invalid — reject zero-rate claim
throw new Error(`vat_${data.status}`);
}Checkout Flow for Custom E-commerce Backends
export async function processCheckout(order: OrderRequest) {
let taxTreatment: TaxTreatment;
try {
taxTreatment = await determineTaxTreatment(
{ country: order.billingCountry, vatNumber: order.vatNumber },
EU_VAT_RATES
);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '';
if (msg.includes('vat_inactive')) {
return { error: 'VAT number is no longer registered. Please check your number.' };
}
if (msg.includes('vat_format_invalid')) {
return { error: 'VAT number format is invalid. Check the format for your country.' };
}
// Unknown error — charge VAT and proceed
taxTreatment = { type: 'b2c', vatRate: EU_VAT_RATES[order.billingCountry] ?? 0.20 };
}
const vatAmount = taxTreatment.type === 'b2b_exempt' ? 0
: order.subtotal * (taxTreatment.type === 'b2c' ? taxTreatment.vatRate : 0.20);
return createOrder({
...order,
vatNumber: taxTreatment.type !== 'b2c' ? taxTreatment.vatNumber : null,
companyName: 'companyName' in taxTreatment ? taxTreatment.companyName : null,
vatAmount,
taxTreatment: taxTreatment.type,
});
}Handling VIES Outages in E-commerce
Warning
For e-commerce one-time orders (unlike subscriptions), you cannot issue a credit note if the VAT was wrongly charged. The safest approach when VIES is down: charge standard VAT, record the VAT number as 'pending revalidation', and contact the customer with a VAT credit note once VIES recovers and confirms the number.
Platform-Specific Patterns
- →Shopify: Use a Checkout UI Extension to add the VAT field; validate via a custom backend on checkout validation event. Store the result in customer metafields.
- →WooCommerce: Use the woocommerce_checkout_process hook to validate the EU VAT field before order creation. Return a wc_add_notice error for invalid numbers.
- →Magento 2: Create a custom observer on checkout_submit_before. Call the TaxID API from the observer and throw a LocalizedException for invalid numbers.
- →Custom checkout: Call the TaxID API before persisting the order. Return a 422 with a user-facing error message for format_invalid and inactive; proceed with VAT applied for service_unavailable.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.