EU VAT Directive 2006/112/EC requires that a VAT number is valid at the time of supply for zero-rate treatment to apply. That phrase — 'at the time of supply' — is the reason 'validate at signup and cache forever' is legally insufficient. A company can be deregistered from VAT between their signup and their next invoice. If you apply zero-rate to that invoice without re-validating, the VAT liability falls on you, not your customer. A real-time validation API called at each transaction point is the correct architecture.
What the EU VAT Directive Actually Requires
Article 138 of the Directive exempts intra-community supplies from VAT when the buyer is a taxable person — i.e., a business with a valid VAT number in another EU member state. Article 131 makes this conditional on the supplier having 'proof' that the conditions are met. Council Regulation 282/2011 Article 25 defines acceptable proof as including confirmation from VIES. This means a VIES query result, dated at or before the time of supply, is the document your tax authority will ask for in an audit.
Note
A VIES 'active' response is considered conclusive evidence of VAT registration under Article 31a of Council Regulation 282/2011. Store the `request_id` from every TaxID API call alongside your transaction record — this is your audit evidence.
Validation Points in a SaaS Billing Lifecycle
| Event | Validate? | Reason |
|---|---|---|
| Customer signup with VAT number | Yes | Initial check before any invoicing |
| First subscription invoice | Yes | First taxable supply — verify status at supply time |
| Recurring monthly/annual invoice | Yes (re-validate) | Registration can lapse between billing cycles |
| One-time purchase | Yes | Validate before creating the charge |
| Customer updates their VAT number | Yes | New number requires fresh validation |
| Annual compliance audit | Yes (batch) | Verify all current reverse-charge customers |
Real-time vs Cached Validation
For transaction-level compliance, 'real-time' means the validation result is fresh at the time of the invoice — not necessarily that you call the API at the millisecond the invoice is generated. A result from within the previous 24 hours is considered real-time for compliance purposes, because VIES itself does not update faster than that. This means you can cache validation results for up to 24 hours without compromising compliance, as long as the cached result was obtained at or before the time of supply.
import { prisma } from './prisma';
export async function validateForInvoice(
customerId: string,
vatNumber: string
): Promise<{ canZeroRate: boolean; auditRef: string | null }> {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Check for a fresh cached result
const cached = await prisma.vatValidation.findFirst({
where: {
customerId,
vatNumber: vatNumber.toUpperCase(),
checkedAt: { gte: twentyFourHoursAgo },
status: { not: 'service_unavailable' },
},
orderBy: { checkedAt: 'desc' },
});
if (cached) {
return {
canZeroRate: cached.status === 'active',
auditRef: cached.requestId,
};
}
// No fresh result — call the 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();
// Persist the result for audit trail
await prisma.vatValidation.create({
data: {
customerId,
vatNumber: vat,
countryCode: country,
status: data.status,
isValid: data.valid,
companyName: data.company_name,
requestId: data.request_id,
checkedAt: new Date(),
},
});
return {
canZeroRate: data.status === 'active',
auditRef: data.request_id,
};
}The Audit Trail Schema
Every validation call that results in a zero-rated invoice should produce an audit record. This is the database schema that satisfies most EU tax authority requirements:
model VatValidation {
id String @id @default(cuid())
customerId String
vatNumber String
countryCode String
status String // active | inactive | format_invalid | service_unavailable
isValid Boolean
companyName String?
address String?
requestId String? // TaxID API request_id — audit reference
checkedAt DateTime @default(now())
invoiceId String? // Link to the invoice this validation supported
customer Customer @relation(fields: [customerId], references: [id])
@@index([vatNumber])
@@index([customerId, checkedAt])
}VIES Downtime and the 'Good Faith' Defence
When VIES returns `service_unavailable`, you have two compliant options: charge standard VAT and issue a credit note once VIES recovers, or apply zero-rate provisionally under the 'good faith' provisions of Article 138(1a) — which requires documenting that you attempted validation and the registry was unavailable. The TaxID API logs every `service_unavailable` response, and the `request_id` proves you attempted validation at a specific time.
Tip
Log every `service_unavailable` response with timestamp, VAT number, and request_id. If you choose to apply zero-rate provisionally, this log is the evidence that your attempt was genuine and the unavailability was systemic, not a workaround.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.