EORI Number Validation in Python — EU Customs Identifier
EORI numbers identify businesses trading across EU borders. Every import declaration, export license, and customs procedure requires one. Here's how to validate them properly in Python — including live checks against the European Commission registry.
In this guide
1. What is an EORI number?
An EORI (Economic Operators Registration and Identification) number is a unique identifier assigned to businesses and individuals who import or export goods in the European Union. Introduced in 2009 under EU Regulation 312/2009, it replaced the older national trader reference numbers with a single, EU-wide system.
Every customs declaration filed within the EU must include a valid EORI number. Without one, goods cannot clear customs — shipments are held, and delays cost money. The number is issued by the customs authority of the EU member state where the business is established and is valid across all 27 member states.
Since Brexit, the UK issues its own EORI numbers with the GB prefix. Businesses trading with both the EU and the UK may need two separate EORI numbers — one for each customs territory.
2. EORI structure by country
An EORI number always starts with a two-letter ISO 3166-1 alpha-2 country code, followed by a country-specific identifier. For most EU member states, the identifier is the national tax or VAT number. The total length and format vary by country.
| Country | Prefix | Identifier format | Example |
|---|---|---|---|
| Germany | DE | Up to 15 digits | DE123456789012345 |
| France | FR | Up to 15 alphanumeric | FR12345678901234 |
| Italy | IT | Up to 15 digits | IT12345678901 |
| Poland | PL | Up to 15 digits (NIP-based) | PL123456789000000 |
| Netherlands | NL | Up to 15 alphanumeric | NL123456789 |
| Spain | ES | Up to 15 alphanumeric | ESA12345678 |
| United Kingdom | GB | 12 digits or GB+VAT | GB123456789000 |
3. Why EORI validation matters
Customs compliance
EU customs authorities require a valid EORI on every import and export declaration. An invalid EORI means your declaration is rejected at the border, causing delays, storage fees, and potential fines.
Supply chain automation
Freight forwarders, logistics platforms, and ERP systems process thousands of declarations daily. Validating EORI numbers at the point of data entry prevents errors from propagating through the supply chain and causing downstream rejections.
Partner onboarding and KYC
When onboarding new suppliers or customers who trade internationally, verifying their EORI number confirms they are registered with customs authorities — an important part of know-your-customer due diligence for cross-border trade.
EC registry verification
Beyond format validation, the European Commission maintains a live registry of active EORI numbers. Checking against this registry confirms the number is not just well-formed but actually assigned and active — critical for high-value shipments and regulatory compliance.
4. The naive approach
A quick regex seems like it should work. After all, EORI numbers are just a country code followed by digits, right?
import re def validate_eori_naive(eori: str) -> bool: """Naive EORI validation — DO NOT use in production.""" pattern = r'^[A-Z]{2}[0-9A-Z]{1,15}$' return bool(re.match(pattern, eori.strip().upper())) # Seems to work... print(validate_eori_naive("PL123456789000000")) # True print(validate_eori_naive("DE123456789012345")) # True # But it also accepts complete nonsense: print(validate_eori_naive("XX999999999999999")) # True (XX is not a country) print(validate_eori_naive("PL1")) # True (too short for Poland) print(validate_eori_naive("GB12345")) # True (invalid GB format)
This approach fails for several reasons:
No country-specific format rules
Each EU member state has its own rules for the identifier part. German identifiers are purely numeric, while Spanish ones start with a letter. A single regex cannot capture 27+ different format rules without becoming unmaintainable.
No live registry check
Format validation alone tells you the number looks correct. It does not tell you if it has actually been issued. A structurally valid EORI that was never registered — or has been revoked — will still pass regex validation.
Maintenance burden
EU regulations evolve. New member states join, format rules change, and the identifier length constraints are updated. Hardcoding validation logic means constantly tracking regulatory changes across 27 countries.
5. The right solution
The IsValid EORI API handles format validation and optional live checks against the European Commission registry in a single GET request. Pass the EORI number and optionally set check=true to query the EC database for real-time status, registered name, and address.
Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.
Full parameter reference and response schema: EORI Validation API docs →
6. Python code example
Using the isvalid-sdk Python SDK or the requests library. Install with pip install isvalid-sdk or pip install requests.
# eori_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) # ── Basic format validation ───────────────────────────────────────────────── result = iv.eori("PL123456789000000") if not result["valid"]: print("Invalid EORI number") else: print(f"Valid EORI — {result['country']} ({result['countryCode']})") print(f"Identifier: {result['identifier']}") print(f"Formatted: {result['formatted']}") # → Valid EORI — Poland (PL) # → Identifier: 123456789000000 # → Formatted: PL123456789000000 # ── With EC registry check ────────────────────────────────────────────────── result = iv.eori("PL123456789000000", check=True) if result.get("ec", {}).get("valid"): print(f"EC registry: {result['ec']['statusDescr']}") print(f"Name: {result['ec']['name']}") # → EC registry: Valid # → Name: EXAMPLE SP. Z O.O.
In a customs declaration handler, you might use it like this with Flask:
# app.py (Flask) import os import requests from flask import Flask, request, jsonify app = Flask(__name__) API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def validate_eori(eori: str, check: bool = False) -> dict: params = {"value": eori} if check: params["check"] = "true" resp = requests.get( f"{BASE_URL}/v0/eori", params=params, headers={"Authorization": f"Bearer {API_KEY}"}, ) resp.raise_for_status() return resp.json() @app.post("/customs-declaration") def customs_declaration(): data = request.get_json() # Validate EORI with live EC check for customs compliance try: eori_check = validate_eori(data["eori"], check=True) except requests.RequestException: return jsonify(error="EORI validation service unavailable"), 502 if not eori_check["valid"]: return jsonify(error="Invalid EORI number format"), 400 ec = eori_check.get("ec", {}) if ec.get("checked") and not ec.get("valid"): return jsonify( error="EORI not found in EC registry", ecStatus=ec.get("statusDescr"), ), 400 # EORI is valid and registered — proceed with declaration declaration = create_declaration( eori=eori_check["formatted"], country=eori_check["countryCode"], trader_name=ec.get("name", data.get("companyName")), goods=data["goods"], ) return jsonify( success=True, declarationId=declaration["id"], trader=ec.get("name"), )
check=True for onboarding and high-value shipments where you need to confirm the EORI is actually registered. For high-throughput form validation where you just need format checks, omit the check parameter to get faster responses without hitting the EC registry.7. cURL example
Basic format validation of a Polish EORI:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=PL123456789000000"
With EC registry check:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=PL123456789000000&check=true"
Validate a German EORI:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=DE123456789012345"
8. Understanding the response
Basic validation response (without EC check):
{ "valid": true, "countryCode": "PL", "country": "Poland", "identifier": "123456789000000", "formatted": "PL123456789000000" }
Response with EC registry check (check=true):
{ "valid": true, "countryCode": "PL", "country": "Poland", "identifier": "123456789000000", "formatted": "PL123456789000000", "ec": { "checked": true, "valid": true, "statusDescr": "Valid", "name": "EXAMPLE SP. Z O.O.", "street": "UL. PRZYKLADOWA 1", "postalCode": "00-001", "city": "WARSZAWA", "country": "PL" } }
Invalid EORI:
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the EORI number has a valid format |
| countryCode | string | 2-letter ISO 3166-1 country code from the EORI prefix |
| country | string | Full country name |
| identifier | string | The national identifier portion (after the country code) |
| formatted | string | Normalised EORI number (uppercase, no spaces) |
| ec.checked | boolean | Whether the EC registry was queried |
| ec.valid | boolean | Whether the EORI is registered and active in the EC database |
| ec.statusDescr | string | Human-readable status from the EC registry |
| ec.name | string | Registered business name from the EC database |
| ec.street | string | Registered street address |
| ec.postalCode | string | Registered postal code |
| ec.city | string | Registered city |
ec object is only present when you pass check=true. Without it, you only get format validation. For customs compliance, always use the EC check to confirm the EORI is actually registered and active.9. Edge cases to handle
UK EORI numbers post-Brexit
Since January 2021, UK businesses trading with the EU need both a GB EORI (for UK customs) and an EU EORI (issued by the EU member state of first import). GB EORI numbers are not in the EC registry — they must be validated through the UK's own system.
result = validate_eori("GB123456789000", check=True) # For GB numbers, ec.checked may be False — the EC registry # does not cover UK-issued EORI numbers post-Brexit. if result["countryCode"] == "GB": # Format is valid, but EC check is not applicable print("GB EORI — format valid, EC registry not available")
EORI vs. VAT number confusion
Many businesses assume their VAT number is their EORI number. In some countries (e.g. Germany), the EORI uses a different identifier than the VAT number. When collecting EORI numbers in your forms, clearly label the field as "EORI number" and provide an example format for the user's country. Consider adding a link to the EC EORI lookup tool so users can verify their own number if unsure.
Input normalisation
Users may enter EORI numbers with spaces, dashes, or mixed case. The API normalises input automatically — it strips whitespace, removes separators, and uppercases the value. Pass the raw user input directly without pre-processing. The formatted field in the response gives you the canonical form.
EC registry downtime
The EC EORI registry is an external service that occasionally experiences downtime. When using check=true, handle the case where the EC check is unavailable gracefully — fall back to format validation and retry the EC check later.
result = validate_eori(eori, check=True) if result["valid"]: ec = result.get("ec", {}) if ec.get("checked"): # Full validation — EC registry responded handle_verified_eori(result) else: # Format valid, but EC registry unavailable # Accept provisionally and verify later handle_provisional_eori(result)
Northern Ireland protocol
Businesses in Northern Ireland may have EORI numbers starting with XI for EU customs purposes, in addition to their GB EORI. The XI prefix is valid and can be checked against the EC registry, unlike GB numbers.
10. Summary
See also
Validate EORI numbers instantly
Free tier includes 100 API calls per day. No credit card required. Format validation plus optional live checks against the European Commission EORI registry.