VAT Guard: a resilient EU VAT ID validation API
I’m building VAT Guard — a high-reliability API for validating EU VAT IDs (VIES) with caching, an audit trail, and sane developer ergonomics.
The goal is simple: fewer flaky calls, consistent data, and evidence you can actually show to auditors.
What it does
- Real VIES checks with smart fallbacks (per-member-state endpoints where available).
- Deterministic caching (configurable TTL + cache staleness headers) so you don’t hammer VIES.
- Audit trail: signed evidence for each check (inputs, source, timestamps, result), exportable for compliance.
- Bulk validation: CSV upload + async processing with progress + downloadable results.
- Webhooks for async results and change events (e.g., a VAT ID flips from valid → invalid).
- Rate-limit protection & retries so your app doesn’t explode when VIES has a bad day.
- Clean JSON schema with strict types, ISO timestamps, and stable field names.
- Optional GB add-on (HMRC VAT number lookup) on the roadmap.
TL;DR: Call one endpoint, get a stable answer + evidence. I deal with VIES’ mood swings so you don’t have to.
Why not just call VIES yourself?
Because VIES can be slow, transiently down, or inconsistent across member states. You also need caching, throttling, retries, and an audit log. VAT Guard wraps all of that into one predictable API with idempotency, observability, and compliance-friendly artifacts.
Architecture at a glance
Multi-channel resolver
A resolver tries the primary VIES service, then falls back to country-specific channels where possible. Failures are classified (network vs. semantic) and retried with jittered backoff.
Caching & consistency
Results are cached by (country_code, vat_number) with a TTL you control. Responses include headers like:
x-vatguard-cache: hit|miss|stale
x-vatguard-checked-at: 2025-09-07T12:00:12Z
Audit trail & evidence
Every validation produces an immutable record: request hash, source channel, timestamps, and the normalized result. You can export CSV/JSON for audits.
Rate limits & backpressure
Token-bucket per API key; graceful 429 with Retry-After. Backpressure propagates cleanly so your workers can pause instead of thrashing.
Availability & retries
Multi-region workers with health probes and circuit breakers. Transient failures trigger retry; hard failures return structured errors.
API shape
Validate a single VAT ID
Request
POST /v1/validate
Content-Type: application/json
Authorization: Bearer {{YOUR_API_KEY}}
{
"country_code": "SK",
"vat_number": "2020202020",
"include_evidence": true
}
Response
{
"valid": true,
"normalized": {
"country_code": "SK",
"vat_number": "2020202020"
},
"entity": {
"name": "Example s.r.o.",
"address": "Príkladná 1, 010 01 Žilina, Slovakia"
},
"source": "vies",
"checked_at": "2025-09-07T12:04:33Z",
"cache": { "status": "hit", "ttl_seconds": 82800 },
"audit_id": "aud_01JABCDEF123",
"request_id": "req_01JXYZ987654",
"links": {
"evidence": "https://api.vatguard.eu/v1/audit/aud_01JABCDEF123"
}
}
Webhook (async/bulk results)
Webhook deliveries are HMAC-SHA256 signed.
POST /webhooks/vatguard
X-VatGuard-Signature: t=1694099999,v1=ab12cd...
{
"type": "validation.completed",
"request_id": "req_01JXYZ987654",
"data": { ...same payload as /validate... }
}
Verify signature (Laravel)
<?php
$payload = request()->getContent();
$sig = request()->header('X-VatGuard-Signature');
[$tPart, $v1Part] = explode(',', $sig);
[$_, $ts] = explode('=', $tPart);
[$__, $v1] = explode('=', $v1Part);
$calc = hash_hmac('sha256', "t={$ts}.{$payload}", config('services.vatguard.webhook_secret'));
abort_unless(hash_equals($calc, $v1), 401, 'Invalid signature');
Laravel example (server-side call)
$resp = Http::withToken(config('services.vatguard.key'))
->post('https://api.vatguard.eu/v1/validate', [
'country_code' => 'DE',
'vat_number' => '123456789',
'include_evidence' => true,
])
->throw()
->json();
if ($resp['valid']) {
// proceed with zero-rate invoice, etc.
}
Bulk validation
Upload a CSV with country_code,vat_number columns. You’ll receive a job_id, a progress endpoint, and a webhook on completion. The result CSV mirrors inputs plus valid, name, address, checked_at, and error (if any).
country_code,vat_number
SK,2020202020
DE,123456789
Security & compliance
- Idempotency via
Idempotency-Keyheader. - Signed webhooks (timestamped HMAC) and IP allowlisting.
- Minimal PII retention; short evidence retention window (configurable).
- GDPR-friendly: only store what’s required for the audit trail, purge on schedule.
Observability
- Every response includes a
request_idfor log correlation. - Headers expose cache state and source channel.
- Error contracts are typed:
validation_error,upstream_unavailable,rate_limited,bad_request.
Status & roadmap
- ✅ Core validate endpoint, caching, audit trail
- 🚧 Dashboard, CSV export UI, team API keys
- 🧩 HMRC GB VAT support (add-on)
- 🛡️ SLA + multi-region failover