Reverse charge VAT sounds simple: instead of you charging and remitting VAT, the buyer does. But implementing it in a SaaS billing system has four discrete steps, each with specific code requirements and compliance implications. Miss any one of them and you either create VAT liability or generate invoices that fail an audit. This guide covers all four steps with working TypeScript examples.
When reverse charge applies: the 3 conditions
Reverse charge for cross-border digital services applies when all three of these conditions are met simultaneously. If any condition fails, you are in B2C territory and must apply the customer's country VAT rate via OSS.
- 1.Both your business and the customer's business are VAT-registered in EU member states
- 2.The customer is registered in a different member state from your seller entity
- 3.The customer's VAT number is confirmed active in VIES at the time of supply
Step 1: Detecting a reverse charge customer
Detection happens at two points: during account creation (when the customer first provides a VAT number) and before each invoice is generated (to catch deregistrations). The classifier below handles both entry points.
export async function detectReverseCharge(
sellerCountry: string, // Your VAT registration country (e.g. 'ES')
customerCountry: string,
vatNumber: string
): Promise<{ eligible: boolean; status: string; companyName?: string; requestId?: string }> {
// Same country: no reverse charge even with a valid VAT number
if (customerCountry === sellerCountry) {
return { eligible: false, status: 'domestic_supply' };
}
const res = await fetch(
`https://www.taxid.dev/api/v1/validate/${customerCountry}/${vatNumber}`,
{
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
signal: AbortSignal.timeout(8000),
}
);
const data = await res.json();
return {
eligible: data.status === 'active' || data.status === 'service_unavailable',
status: data.status,
companyName: data.company_name,
requestId: data.request_id,
};
}Step 2: Validating before applying zero-rate
At checkout, validate in real time and store the full result — not just the boolean. You need the request_id for audit purposes and the company_name for invoice generation. If VIES is unavailable, allow the customer through but flag for re-validation.
async function processVatAtCheckout(
customerId: string,
countryCode: string,
vatNumber: string
) {
const result = await detectReverseCharge(
process.env.SELLER_COUNTRY!, countryCode, vatNumber
);
// Persist validation record — required for compliance audit
await db.vatValidations.create({
data: {
customerId,
vatNumber,
countryCode,
status: result.status,
companyName: result.companyName ?? null,
requestId: result.requestId ?? null,
validatedAt: new Date(),
source: 'checkout',
},
});
if (result.status === 'service_unavailable') {
// VIES is temporarily down — allow through, schedule re-validation
await scheduleRevalidation(customerId, 'vies_unavailable');
return { proceed: true, reverseCharge: true, requiresRevalidation: true };
}
return {
proceed: result.eligible || result.status === 'format_invalid',
reverseCharge: result.eligible,
companyName: result.companyName,
};
}Warning
Never treat service_unavailable as invalid. VIES is down approximately 2% of the time. Rejecting a valid customer because VIES was temporarily unreachable creates churn and is legally unnecessary — you are not required to block the sale, only to validate when possible.
Step 3: Generating the compliant invoice
A reverse-charge invoice must satisfy Article 226 of the EU VAT Directive. The most commonly omitted elements are the explicit 'reverse charge' text and the Article 196 reference. Without these, the invoice is non-compliant even if the underlying VAT number was correctly validated.
| Invoice field | Required? | Example value |
|---|---|---|
| Your VAT number | Yes | ESB12345678 |
| Customer's VAT number | Yes | DE123456789 (the VIES-validated number) |
| 'Reverse charge' notation | Yes | "VAT: Reverse charge (Article 196, EU VAT Directive)" |
| Net amount | Yes | €1,200.00 |
| VAT rate | Yes | 0% |
| VAT amount | Yes | €0.00 |
| Invoice date | Yes | 2026-06-05 |
| Sequential invoice number | Yes | INV-2026-00142 |
Step 4: The monthly re-validation job
Businesses deregister. A VAT number that was valid at onboarding can become inactive at any time. Applying reverse charge based on a stale validation exposes you to the same liability as never validating — the obligation is to verify the status 'at the time of supply', which for subscriptions means each billing cycle.
// Run as a cron job on the 1st of each month, before invoice generation
export async function monthlyVatRevalidationJob() {
const customers = await db.customer.findMany({
where: {
vatStatus: 'active',
vatCountryCode: { not: process.env.SELLER_COUNTRY },
},
select: { id: true, vatNumber: true, vatCountryCode: true, email: true },
});
let statusChanges = 0;
for (const customer of customers) {
// Rate-limit: 1 request per 200ms to stay within API limits
await new Promise((r) => setTimeout(r, 200));
const result = await detectReverseCharge(
process.env.SELLER_COUNTRY!,
customer.vatCountryCode,
customer.vatNumber
);
if (result.status !== 'active' && result.status !== 'service_unavailable') {
await db.customer.update({
where: { id: customer.id },
data: { vatStatus: result.status, vatStatusChangedAt: new Date() },
});
await notifyFinanceTeam(customer, result.status);
statusChanges++;
}
}
console.log(
`Re-validation complete. ${customers.length} checked, ${statusChanges} status changes.`
);
}Stripe-specific: setting tax_exempt correctly
If you use Stripe for billing, set the customer's tax_exempt property to 'reverse' when reverse charge applies. This signals to Stripe Tax not to apply VAT and ensures the invoice exported from Stripe is annotated correctly. You still need to store your own validation record — Stripe does not perform VIES validation on your behalf.
const result = await detectReverseCharge(SELLER_COUNTRY, countryCode, vatNumber);
if (result.eligible) {
await stripe.customers.update(stripeCustomerId, {
tax_exempt: 'reverse',
metadata: {
vat_number: vatNumber,
vat_company: result.companyName ?? '',
vat_validated_at: new Date().toISOString(),
vat_request_id: result.requestId ?? '',
},
});
} else {
// Not eligible for reverse charge
await stripe.customers.update(stripeCustomerId, { tax_exempt: 'none' });
}Related resources
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.