How to Validate EORI Numbers in Node.js
EORI numbers are mandatory for customs operations in the EU. A simple regex won't verify whether an operator is actually registered — here's how to validate EORI numbers properly with a single API call.
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 interact with customs authorities in the European Union. It was introduced by EU Regulation (EC) No 312/2009 to create a common identification system across all EU member states, replacing the patchwork of national identifiers that existed before.
Every economic operator — whether an importer, exporter, freight forwarder, or customs broker — must have a valid EORI number to submit customs declarations, apply for customs decisions, or participate in any customs procedure within the EU. Without one, goods cannot clear customs.
EORI numbers are assigned by the national customs authority of the member state where the business is established. The format always begins with a two-letter ISO 3166-1 alpha-2 country code, followed by a national identifier — typically the company's existing tax or registration number.
Key situations where an EORI number is required:
- Filing customs declarations (import and export)
- Applying for Authorised Economic Operator (AEO) status
- Requesting binding tariff information (BTI)
- Using customs simplifications and transit procedures
- Interacting with the Union Customs Code (UCC) IT systems
2. EORI structure
An EORI number consists of two parts: a two-letter country code (ISO 3166-1 alpha-2) followed by a national identifier. The national part varies significantly between countries — some use existing tax identification numbers, others use dedicated customs registration numbers.
The maximum total length is 17 characters (2-letter country code + up to 15 characters for the national identifier). Here are the formats used by major EU member states:
| Country | Prefix | National identifier format |
|---|---|---|
| Germany | DE | Up to 15 digits (customs number or tax ID) |
| Poland | PL | 10 digits (NIP tax identification number) |
| France | FR | 14 digits (SIRET number) |
| Italy | IT | 11 or 16 chars (fiscal code / partita IVA) |
| Spain | ES | 9 chars (NIF/CIF tax identification) |
| Netherlands | NL | 9 digits + B01 suffix or RSIN number |
| Belgium | BE | 10 digits (enterprise number) |
| Czech Republic | CZ | 8-10 digits (ICO identification number) |
| Austria | AT | Up to 15 alphanumeric characters |
| Ireland | IE | 8-9 chars (tax reference number) |
3. Why EORI validation matters
Submitting a customs declaration with an invalid or unregistered EORI number has real consequences. Unlike a typo in an email address, an incorrect EORI can halt the physical movement of goods at the border.
Customs clearance delays
An invalid EORI will cause the customs declaration to be rejected outright. Goods sit in a customs warehouse or at the port while the issue is resolved. For perishable goods or time-sensitive shipments, this can mean total loss of the consignment. Even for non-perishable goods, daily storage fees at ports and airports add up quickly.
Regulatory fines
National customs authorities can impose penalties for incorrect declarations. In some jurisdictions, repeated submissions with invalid EORI numbers can trigger enhanced scrutiny of all future shipments, effectively red-flagging your business in customs systems.
Supply chain disruptions
Modern supply chains are tightly coupled. A delayed shipment at one point cascades through the entire chain — production lines stop, retail shelves go empty, contractual SLAs are breached. Validating EORI numbers before submission prevents these cascading failures.
TARIC and customs declarations
The EORI number is a mandatory field in TARIC-based customs declarations, the Import Control System (ICS2), the Export Control System (ECS), and the New Computerised Transit System (NCTS). Automated customs systems will reject declarations where the EORI number does not match the European Commission's EORI database.
4. The naive approach and why it fails
The first instinct is to validate the format with a regular expression. Something like this:
// ❌ DO NOT USE — this gives a false sense of security const EORI_REGEX = /^[A-Z]{2}[0-9A-Z]{1,15}$/; function validateEori(eori) { return EORI_REGEX.test(eori.toUpperCase().replace(/\s/g, '')); } validateEori('PL1234567890'); // true ✓ (looks right) validateEori('PL0000000000'); // true ✗ (passes regex but is not registered) validateEori('DE999999999999'); // true ✗ (format OK but not a real EORI) validateEori('XX123456789'); // true ✗ (XX is not a valid country code!) validateEori('FR12345678901234'); // true ✓ (correct SIRET-based format) validateEori('IT12345'); // true ✗ (too short for Italian EORI)
This regex approach has several fundamental problems:
No country-specific format checks
A catch-all regex cannot enforce that a Polish EORI has exactly 10 digits after the country code, or that a French EORI must contain a valid 14-digit SIRET number. Each country has its own rules for what constitutes a valid national identifier.
No registration verification
A syntactically valid EORI number may never have been registered, or it may have been revoked. The European Commission maintains the official EORI validation service — the only authoritative way to confirm that an EORI number is currently active is to query this registry.
No company details
When building customs software, you often need to verify that the EORI number belongs to the expected company. A regex tells you nothing about the registered operator's name, address, or status. Only the EC registry can provide this information.
5. The right solution: one API call
Instead of maintaining country-specific regex patterns and integrating directly with the EC EORI validation service, use the IsValid EORI API. A single GET request handles format validation, country detection, and optional registration verification against the European Commission's official EORI database.
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. Node.js code example
Using the IsValid SDK or the native fetch API.
import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Example usage ──────────────────────────────────────────────────────────── // Basic format validation const basic = await iv.eori('PL1234567890'); console.log(basic.valid); // true console.log(basic.countryCode); // "PL" console.log(basic.country); // "Poland" // With EC registry check — verifies actual registration status const result = await iv.eori('PL1234567890', { check: true }); if (!result.valid) { console.log('Invalid EORI format'); } else if (result.ec?.checked && !result.ec.valid) { console.log('Format OK, but not found in EC EORI registry'); console.log('Status:', result.ec.statusDescr); } else if (result.ec?.checked && result.ec.valid) { console.log(`Registered operator: ${result.ec.name}`); console.log(`Address: ${result.ec.street}, ${result.ec.postalCode} ${result.ec.city}`); } else { console.log(`Format valid — ${result.country}`); }
In a customs declaration form handler, you might use it like this:
// routes/customs.js (Express) app.post('/declaration', async (req, res) => { const { eori, consignmentDetails, ...rest } = req.body; let eoriCheck; try { eoriCheck = await validateEori(eori, { check: true }); } catch { return res.status(502).json({ error: 'EORI validation service unavailable' }); } if (!eoriCheck.valid) { return res.status(400).json({ error: 'Invalid EORI number format' }); } if (eoriCheck.ec?.checked && !eoriCheck.ec.valid) { return res.status(400).json({ error: 'EORI number is not registered in the EC database', status: eoriCheck.ec.statusDescr, }); } // Proceed with the customs declaration await submitDeclaration({ eori, operatorName: eoriCheck.ec?.name, country: eoriCheck.country, consignmentDetails, ...rest, }); res.json({ success: true, operator: eoriCheck.ec?.name }); });
result.valid) from EC registry status (result.ec.valid). A format-valid EORI that is not in the EC registry may simply not have been registered yet, or may belong to a non-EU country that is not in the EC system.7. cURL example
Basic format validation only (fast, no EC registry call):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=PL1234567890"
With EC EORI registry check (slower — queries the European Commission servers):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=PL1234567890&check=true"
Checking a German EORI number with a longer identifier:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/eori?value=DE123456789012345&check=true"
8. Understanding the response
Basic format validation response (without EC check):
{ "valid": true, "countryCode": "PL", "country": "Poland", "identifier": "1234567890", "formatted": "PL1234567890" }
Response with EC registry check enabled — a registered Polish operator:
{ "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. PRZYKŁADOWA 1", "postalCode": "00-001", "city": "WARSZAWA" } }
Response when the EORI number is not found in the EC registry:
{ "valid": true, "countryCode": "DE", "country": "Germany", "identifier": "999999999999", "formatted": "DE999999999999", "ec": { "checked": true, "valid": false, "statusDescr": "Not found" } }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Format and country-specific structure are correct |
| countryCode | string | 2-letter ISO 3166-1 country code extracted from the EORI |
| country | string | Full country name |
| identifier | string | The national identifier part (after the country code) |
| formatted | string | Normalised EORI number (uppercase, no spaces) |
| ec.checked | boolean | true if the EC EORI service was queried successfully |
| ec.valid | boolean | Whether the EORI is actively registered in the EC database |
| ec.statusDescr | string | Human-readable status from the EC service (e.g. "Valid", "Not found") |
| ec.name | string | null | Registered operator name from the EC database |
| ec.street | string | null | Street address of the registered operator |
| ec.postalCode | string | null | Postal code of the registered operator |
| ec.city | string | null | City of the registered operator |
When check is omitted or false, the ec field is not included in the response. When the EC service is unreachable, ec.checked is false and ec.reason indicates the failure reason (e.g. "unavailable").
9. Edge cases
(a) EORI not found in the EC registry
A format-valid EORI that returns ec.valid: false may be a newly registered number that has not yet propagated to the EC database, a number that was revoked, or simply a number that was never registered. Do not automatically reject it in all cases — consider the business context.
// Handle EORI not found in EC registry if (result.valid && result.ec?.checked && !result.ec.valid) { if (result.ec.statusDescr === 'Not found') { // Could be newly registered — allow with manual review flag logger.warn('EORI not in EC registry:', eoriNumber); return { valid: true, registrationStatus: 'not_found', requiresReview: true }; } }
(b) EC service downtime
The European Commission's EORI validation service, like VIES for VAT numbers, experiences periodic downtime for maintenance and occasional outages. When the EC service is unavailable, the API returns ec.checked: false with ec.reason: "unavailable". Do not reject the EORI number in this case — treat it as "format valid, registry check inconclusive" and retry later.
if (result.ec && !result.ec.checked) { if (result.ec.reason === 'unavailable') { // EC service is temporarily down — do not reject logger.warn('EC EORI service unavailable for', eoriNumber); return { valid: result.valid, ecStatus: 'unavailable', message: 'Format validated. EC registry check will be retried.', }; } }
(c) GB post-Brexit — XI for Northern Ireland
After Brexit, the United Kingdom left the EU customs union. GB-prefixed EORI numbers are no longer in the EC EORI database. However, Northern Ireland businesses that trade with the EU under the Windsor Framework use the XI prefix — these are registered in the EC system and can be validated against it.
If your system handles UK-EU trade, you need to distinguish between GB and XI prefixes:
async function validateEoriWithBrexitHandling(eoriNumber) { const result = await validateEori(eoriNumber, { check: true }); if (result.countryCode === 'GB') { // GB EORI — not in EC database, validate format only // For customs purposes, check with HMRC instead return { ...result, note: 'GB EORI — not in EC registry. Verify with HMRC if needed.', }; } if (result.countryCode === 'XI') { // Northern Ireland — should be in EC database if (result.ec?.checked && !result.ec.valid) { return { ...result, note: 'XI EORI not found in EC registry — may need re-registration under Windsor Framework.', }; } } return result; }
(d) Format differences per country
Unlike VAT numbers or IBANs, EORI national identifiers vary dramatically between countries. Some countries use purely numeric identifiers (Poland: 10-digit NIP), others allow alphanumeric characters (Austria, UK). Some countries pad the national identifier to a fixed length, others allow variable length.
The API handles all these country-specific rules internally. You should not attempt to validate the national identifier format yourself — pass the raw EORI to the API and let it determine whether the format matches the issuing country's rules.
// ❌ Don't do this — you'll get country-specific rules wrong function isValidEoriFormat(eori) { const countryCode = eori.slice(0, 2); const identifier = eori.slice(2); switch (countryCode) { case 'PL': return /^\d{10}$/.test(identifier); // What about 13-digit REGON-based? case 'DE': return /^\d{1,15}$/.test(identifier); // Some DE EORIs have letters case 'FR': return /^\d{14}$/.test(identifier); // Not always SIRET-based // ... 24 more countries to maintain } } // ✅ Do this instead — let the API handle country-specific validation const result = await iv.eori(eoriNumber);
Network failures in your code
Always wrap the API call in a try/catch. A network timeout should not cause your customs declaration process to crash — decide upfront whether to fail open or closed on API unavailability.
async function validateEoriSafe(eoriNumber, options = {}) { try { return await validateEori(eoriNumber, options); } catch (err) { // Log and decide: fail open (allow) or fail closed (reject) logger.error('EORI validation failed:', err.message); return { valid: null, error: 'validation_unavailable' }; } }
10. Summary
See also
Validate EORI numbers instantly
Free tier includes 100 API calls per day. No credit card required. Format validation for all EU member states plus optional EC registry verification.