Building EuroValidate: replacing 5 government APIs with one REST call
Solo founder, €7/mo server, 2 weeks to 4.4/5 QA score. The technical story.
Building EuroValidate: Replacing 5 Government APIs with One REST Call
There are five different government APIs you need to validate European business data. VIES is SOAP. EORI is SOAP. HMRC is REST. GLEIF is REST. IBAN is offline computation. Each has different authentication, different response formats, different failure modes, different country-code conventions, and different ideas about what "available" means.
I built EuroValidate because I got tired of writing the same integration code in every project. This is the story of how it went from "I should just wrap these APIs" to a production system with 30 circuit breakers, 130 bank entries, and a confidence scoring engine -- built solo, from Portugal, on infrastructure that costs less than a coffee per day.
The problem: five APIs, five headaches
Here is what you need to validate EU business data end-to-end:
| Data | Source | Protocol | Auth | Quirks |
| VAT validation | VIES | SOAP | None | Germany/Spain hide names, Greece uses EL not GR, global rate limit |
| EORI validation | EC endpoint | SOAP | None | Separate from VIES, different WSDL |
| UK VAT + EORI | HMRC | REST | None | Different API entirely, JSON responses |
| Company data (LEI) | GLEIF | REST | None | Free, reliable, but only for LEI holders |
| IBAN validation | MOD 97 algorithm | Offline | N/A | Deterministic, but bank lookup needs a registry |
If you are building a fintech KYB flow, you need at least three of these. If you are building an e-commerce checkout with EU tax compliance, you need four. If you are doing cross-border logistics, you need all five.
Each integration takes 2-4 days to build properly (with error handling, retries, caching). That is 10-20 days of work before you even start on your actual product. And then you maintain five integrations forever.
Why existing solutions did not work
I looked at every competitor I could find. Over 30 of them. The landscape breaks into three categories:
VAT-only tools (Vatstack, Vatchecker, Abstract VAT): They handle VAT validation well but do not touch IBAN, EORI, or company data. You still need three more integrations.
IBAN-only tools (Ibanapi.com, openiban.com): Same problem from the other direction. You get IBAN validation but nothing else.
Expensive unified tools (Surepass, Veriphy): They cover multiple data types but price at enterprise levels. A startup doing 5,000 validations per month does not need a sales call and a custom contract.
The gap was clear: a unified API that covers all five validation types, at a price point that makes sense for indie developers and small SaaS teams, with reliability engineering that accounts for the fact that government APIs go down constantly.
Architecture decisions
Why FastAPI
FastAPI was the obvious choice for three reasons:
Async-native: VIES calls take 200-2000ms. If you are handling concurrent requests (and you are, since this is an API), blocking on each SOAP call kills throughput. FastAPI's async support lets us run VIES calls in thread executors without blocking the event loop.
Auto-generated OpenAPI 3.1 docs: The API spec is the product. Auto-generated Swagger and Redoc documentation mean the docs are always in sync with the code. No separate doc maintenance.
Pydantic v2 for request/response models: Every response model is typed. Every error follows RFC 7807. The schema drives SDK generation. One source of truth.
Per-country circuit breakers
This was the most important architectural decision. VIES routes requests to 27 national backends. Germany goes down frequently. If you use a single circuit breaker for all of VIES, Germany being down trips the breaker and blocks France, Netherlands, and everyone else.
We run 28 independent VIES breakers (one per member state, with GR and EL aliased since they are the same backend). Plus one for EC EORI and one for HMRC UK. Thirty breakers total.
EU_COUNTRY_CODES: list[str] = [
"AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "EL", "ES",
"FI", "FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV",
"MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK",
]
class CircuitBreakerRegistry:
def __init__(self):
self._vies_breakers = {}
for cc in EU_COUNTRY_CODES:
self._vies_breakers[cc] = _make_breaker(f"vies_{cc}")
# Greece: GR and EL are the same backend
self._vies_breakers["GR"] = self._vies_breakers["EL"]
self._eori_breaker = _make_breaker("eori_ec")
self._hmrc_breaker = _make_breaker("hmrc_uk")
Each breaker trips after 5 consecutive failures and probes after 60 seconds. State transitions are logged for observability.
Dual-layer cache
Redis for hot reads (sub-millisecond). PostgreSQL for persistence (survives restarts). The lookup chain:
- Check Redis (fast path, handles 90%+ of repeat queries)
- Check PostgreSQL (warm path, catches Redis misses after restart)
- Call upstream (slow path, only on cache miss)
VAT data is cached for 24 hours. IBAN validation is purely offline, so there is no cache -- it is always instant. GLEIF data is cached for 7 days (it changes rarely). Bank registry data is cached for 30 days (updated monthly from Bundesbank).
Confidence scoring
Every response includes a confidence level. This was inspired by how financial systems handle data quality -- you never just say "valid" or "invalid" without context.
The scoring rules:
- HIGH: Live upstream response, or deterministic offline check (IBAN MOD 97), or cached data under 1 hour old
- MEDIUM: Cached data between 1-24 hours old
- LOW: Cached data over 24 hours old
- UNKNOWN: Upstream unreachable, no cache exists
Cross-referencing can promote MEDIUM to HIGH when two independent sources (e.g., VIES and GLEIF) agree on an entity. But it never promotes LOW or UNKNOWN -- stale is stale regardless of how many stale sources agree.
This is the feature that makes EuroValidate different from a simple wrapper. Clients can build decision trees:
if confidence == HIGH: auto-approve
if confidence == MEDIUM: auto-approve with note
if confidence == LOW: queue for manual review
if confidence == UNKNOWN: reject or retry later
Tech stack and costs
The full stack:
Python 3.12+ / FastAPI (async, auto OpenAPI 3.1)
zeep (VIES + EORI SOAP)
httpx (GLEIF + HMRC async REST)
Redis 7 (hot cache)
PostgreSQL 16 (persistent cache, API keys, usage logs)
pybreaker (circuit breakers)
tenacity (retry with exponential backoff + jitter)
Stripe Billing Meters (usage-based billing)
Hetzner CX22 (hosting)
Cloudflare (CDN, DDoS, SSL)
Monthly cost at launch:
| Item | Cost |
| Hetzner CX22 (2 vCPU, 4GB RAM) | ~7.50 EUR |
| Cloudflare (free plan) | 0 EUR |
| Domain | ~1 EUR/mo amortized |
| Stripe | 0 EUR (pay-as-you-go) |
| Sentry (free tier) | 0 EUR |
| Uptime Robot (free tier) | 0 EUR |
| Total | ~10 EUR/mo |
All upstream data sources are free. VIES, EORI, HMRC, GLEIF -- all government or public-interest APIs with no usage fees. The business model is API arbitrage: free government data in, reliable paid API out. The margin is essentially the reliability engineering.
The IBAN validator: 435 lines of offline correctness
The IBAN validator deserves its own mention because it demonstrates a different approach. Unlike VIES (which requires network calls to unreliable SOAP services), IBAN validation is entirely offline and deterministic.
The validator implements:
- Format normalization: Strip spaces, dashes, uppercase
- Country-specific length validation: 88 countries supported (from the SWIFT IBAN Registry)
- MOD 97 check digit verification: ISO 7064 algorithm
- BBAN extraction: Country-specific parsing rules
- Bank lookup: BIC and bank name from an in-memory registry of 130 EU banks across 27 countries
- SEPA reachability: Is this IBAN in the Single Euro Payments Area?
A single function call returns all of this:
result = validate_iban("NL91ABNA0417164300")
# IbanResult(
# valid=True,
# country="NL",
# check_digits="91",
# bban="ABNA0417164300",
# bic="ABNANL2AXXX",
# bank_name="ABN AMRO Bank N.V.",
# bank_city="Amsterdam",
# sepa_reachable=True
# )
No network call. No cache. No retry. No circuit breaker. Always HIGH confidence. Response time: effectively zero (CPU computation only).
The bank registry covers all 27 EU member states plus the UK. Each country has 3-6 major clearing banks mapped to their BIC codes. The extraction rules are country-specific -- Germany uses an 8-digit Bankleitzahl, France uses a 5-digit code banque, Netherlands uses 4-letter bank codes, Italy skips the first character (CIN check digit) before extracting the ABI code.
The Greece bug and other war stories
The Greece EL vs GR issue is a rite of passage for anyone integrating with VIES. ISO 3166-1 says Greece is GR. VIES says EL (from Ellada, the Greek name). If you send GR to VIES, you get INVALID_INPUT. If you store EL in your database and join it with anything that uses ISO country codes, your joins break.
We handle this with bidirectional mapping at the boundary:
_COUNTRY_TO_VIES: dict[str, str] = {"GR": "EL"}
_VIES_TO_COUNTRY: dict[str, str] = {"EL": "GR"}
Input gets mapped to VIES format. Output gets mapped back to ISO. The rest of the system only sees GR. This seems trivial until you realize that the circuit breaker registry also needs to alias GR and EL to the same breaker instance, not two separate ones.
Other edge cases we handle:
- Germany and Spain never return company names (national law). We return
nullinstead of empty strings and fall back to GLEIF for company identity. - Northern Ireland has dual status: XI for EU customs, GB for UK VAT. Different validation systems, different protocols.
- Monaco uses French VAT prefixes (FR). Canary Islands are outside the EU VAT area despite being Spanish territory.
- VIES returns "---" as a literal string when data is unavailable. Three dashes. Not null. Not empty string. Three dashes.
Solo founder: what I would do differently
Ship the landing page first. I built the API before the landing page. I should have put up a landing page with an email capture, validated demand with SEO content about VIES reliability problems, and then built the API. The content marketing pipeline should start before the code.
Start with fewer endpoints. EuroValidate launched with VAT, IBAN, EORI, company lookup, VAT rates, batch processing, monitoring webhooks, and a status endpoint. That is eight endpoints. I should have launched with VAT and IBAN only, gotten paying customers, then expanded. Two endpoints that work perfectly beat eight that are untested in production.
Invest in test data earlier. EU validation has dozens of edge cases (Greece codes, Northern Ireland, territorial exceptions). I should have built a comprehensive test data matrix on day one instead of discovering edge cases during integration testing.
What is next
The roadmap is driven by EU regulatory changes and developer requests:
ViDA compliance (VAT in the Digital Age): The EU is rolling out mandatory e-invoicing and real-time digital reporting. EuroValidate will provide ViDA-ready validation endpoints that check whether a transaction meets the new requirements before you submit it to your tax authority.
GLEIF bulk import: Pre-warming the cache with the entire GLEIF database (~2.5M entities with LEIs). This means company lookups for LEI holders will always be instant, never hitting the upstream API.
Go SDK: Python and Node.js SDKs are generated from the OpenAPI spec. Go is next, followed by PHP. The spec-first approach means SDK generation is mostly automated.
Expanded bank registry: The current 130-bank registry covers the major clearing institutions. We are working on importing the full Bundesbank SCL-Directory (thousands of German banks) and equivalent registries from other countries.
The numbers
As of launch:
- 130 EU banks in the IBAN registry across 27 countries
- 30 circuit breakers (28 VIES + 1 EORI + 1 HMRC)
- 88 countries supported for IBAN length validation
- 38 SEPA countries in the reachability check
- 4 confidence levels with deterministic scoring
- 3 retry attempts with exponential backoff + jitter for transient VIES errors
- 10 concurrent VIES threads (bounded pool to avoid contributing to MS_MAX_CONCURRENT_REQ)
- ~10 EUR/mo infrastructure cost
The business model is straightforward. Free tier at 100 calls/month for testing. Paid tiers from $19/mo (5,000 calls) to $149/mo (100,000 calls). Enterprise above that. Usage-based billing through Stripe Billing Meters.
The gross margin is 95%+ because every upstream data source is free. You are paying for the reliability layer: caching, circuit breakers, confidence scoring, graceful degradation, unified JSON responses, and the engineering time spent handling every VIES edge case so you do not have to.
Try it
EuroValidate is live at eurovalidate.com. API documentation at api.eurovalidate.com/docs. Free tier, no credit card required.
If you have built VIES integrations before, you know the pain. If you have not, you now know what to expect. Either way, you should not have to write another SOAP client in 2026.
