SaaS companies selling to EU businesses face a VAT compliance challenge that does not exist for one-time e-commerce: the subscription relationship outlasts any single VAT validation. A customer whose VAT number was valid at signup may have their registration cancelled six months later. The correct architecture validates at signup, re-validates before each invoice, stores a full audit trail, and handles all edge cases — plan upgrades, dunning periods, cancelled subscriptions — without creating tax liabilities.
The SaaS VAT Compliance Lifecycle
| Lifecycle event | VAT action required |
|---|---|
| Signup with VAT number | Validate immediately; set tax_exempt if active |
| First invoice | Re-validate if more than 24h since last check |
| Monthly recurring invoice | Re-validate — registration may have lapsed |
| Annual plan renewal | Re-validate — extra caution for high-value invoices |
| Customer updates VAT number | Validate the new number immediately |
| Dunning (payment failure) | Re-validate before retry invoice |
| Subscription cancellation | No action required — VAT on the final invoice was validated |
| Annual audit | Batch re-validate all currently reverse-charged customers |
Signup Flow
interface SignupPayload {
email: string;
vatNumber?: string;
billingCountry: string;
}
async function processSaasSignup(payload: SignupPayload) {
let vatStatus: 'b2b' | 'b2c' | 'pending' = 'b2c';
let companyName: string | null = null;
let vatAuditRef: string | null = null;
if (payload.vatNumber) {
const country = payload.vatNumber.slice(0, 2).toUpperCase();
const vat = payload.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') {
vatStatus = 'b2b';
companyName = data.company_name;
vatAuditRef = data.request_id;
} else if (data.status === 'service_unavailable') {
vatStatus = 'pending'; // re-validate before first invoice
} else {
// invalid — reject at signup; return error to user
throw new Error(`vat_validation_failed:${data.status}`);
}
}
const customer = await db.customer.create({
data: {
email: payload.email,
vatNumber: payload.vatNumber?.replace(/\s/g, '').toUpperCase() ?? null,
vatStatus,
companyName,
vatAuditRef,
vatValidatedAt: vatAuditRef ? new Date() : null,
},
});
return customer;
}Pre-Invoice Re-validation
async function ensureVatValidForInvoice(customerId: string): Promise<boolean> {
const customer = await db.customer.findUnique({ where: { id: customerId } });
if (!customer?.vatNumber) return false; // B2C — no zero-rate
// Re-validate if last check was over 24 hours ago
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
if (customer.vatValidatedAt && customer.vatValidatedAt > oneDayAgo
&& customer.vatStatus === 'b2b') {
return true; // Fresh cache — safe to zero-rate
}
// Stale or pending — re-validate now
const country = customer.vatNumber.slice(0, 2);
const res = await fetch(
`https://taxid.dev/api/v1/validate/${country}/${customer.vatNumber}`,
{ headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` } }
);
const data = await res.json();
await db.customer.update({
where: { id: customerId },
data: {
vatStatus: data.status === 'active' ? 'b2b' : data.status === 'service_unavailable' ? 'pending' : 'b2c',
vatAuditRef: data.request_id,
vatValidatedAt: new Date(),
},
});
return data.status === 'active';
}Handling Subscription Edge Cases
- →Dunning: Re-validate before each retry invoice. A lapsed VAT registration during a failed payment period means the retry invoice should include VAT.
- →Plan upgrade mid-cycle: A prorated invoice is a separate taxable supply. Re-validate before generating the upgrade invoice.
- →VAT number change: When a customer updates their VAT number, immediately validate the new number and update the Stripe customer's tax_exempt status. Log the change with both old and new numbers.
- →Deregistration during a billing cycle: If re-validation detects an inactive VAT number mid-cycle, flag for finance review. Do not retroactively change previous invoices — they were correctly zero-rated when issued.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.