Dávid Kolembus
← Back to portfolio

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

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

Observability

Status & roadmap

Visit vatguard.eu →