Compliance11 min readAlberto García

Mastering EU VAT Compliance with a Real-time Validation API

EU law requires VAT number validation at the time of each transaction, not just at signup. This guide shows how to architect a real-time validation integration that satisfies both the legal requirements and your engineering team.

compliancevateuapiguide

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

EventValidate?Reason
Customer signup with VAT numberYesInitial check before any invoicing
First subscription invoiceYesFirst taxable supply — verify status at supply time
Recurring monthly/annual invoiceYes (re-validate)Registration can lapse between billing cycles
One-time purchaseYesValidate before creating the charge
Customer updates their VAT numberYesNew number requires fresh validation
Annual compliance auditYes (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.

typescriptlib/compliance-validation.ts
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:

typescriptprisma/schema.prisma
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.

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.