Guide · Node.js · SDK · REST API

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.

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.

ℹ️Since July 2021, EORI is also required for all commercial imports into the EU with a value exceeding EUR 150, following the abolition of the low-value consignment relief under the EU VAT e-commerce package. This means even small e-commerce businesses shipping to the EU need an EORI 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:

CountryPrefixNational identifier format
GermanyDEUp to 15 digits (customs number or tax ID)
PolandPL10 digits (NIP tax identification number)
FranceFR14 digits (SIRET number)
ItalyIT11 or 16 chars (fiscal code / partita IVA)
SpainES9 chars (NIF/CIF tax identification)
NetherlandsNL9 digits + B01 suffix or RSIN number
BelgiumBE10 digits (enterprise number)
Czech RepublicCZ8-10 digits (ICO identification number)
AustriaATUp to 15 alphanumeric characters
IrelandIE8-9 chars (tax reference number)
ℹ️Unlike VAT numbers, EORI numbers do not have a universal checksum algorithm. The validity of the national identifier part depends entirely on the issuing country's own rules. A Polish EORI uses the NIP checksum (modulo 11), while a German EORI may use a completely different scheme or none at all.

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.

⚠️If you operate a logistics platform, freight management system, or customs brokerage application, validating EORI numbers at the point of data entry — before they reach any customs system — saves hours of manual intervention and avoids costly delays.

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.

⚠️A regex can confirm the format, but it cannot confirm registration. For customs operations, format validation alone is not enough — you need to verify against the European Commission's EORI database.

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.

Format validation
Country-specific structure and length checks
Country detection
Automatic country code extraction and resolution
EC registry check
Optional live query against the official EC EORI database
Company details
Operator name, address, and status from the registry
27
EU countries
all member states
<50ms
Response time
format check only
100/day
Free tier
no credit card

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 });
});
Always separate format validation (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"
  }
}
FieldTypeDescription
validbooleanFormat and country-specific structure are correct
countryCodestring2-letter ISO 3166-1 country code extracted from the EORI
countrystringFull country name
identifierstringThe national identifier part (after the country code)
formattedstringNormalised EORI number (uppercase, no spaces)
ec.checkedbooleantrue if the EC EORI service was queried successfully
ec.validbooleanWhether the EORI is actively registered in the EC database
ec.statusDescrstringHuman-readable status from the EC service (e.g. "Valid", "Not found")
ec.namestring | nullRegistered operator name from the EC database
ec.streetstring | nullStreet address of the registered operator
ec.postalCodestring | nullPostal code of the registered operator
ec.citystring | nullCity 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;
}
ℹ️The XI prefix was introduced specifically for Northern Ireland under the Northern Ireland Protocol (now the Windsor Framework). Businesses in Northern Ireland that move goods between Northern Ireland and the EU need an XI EORI number in addition to their GB EORI. The XI number is what gets checked against the EC system.

(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

Do not rely on a single regex — EORI formats differ per country
Do not treat format-valid as registered — always verify against the EC database
Use per-country format validation + optional EC registry check
Handle EC service downtime gracefully — do not reject on timeout
Account for GB vs. XI post-Brexit when dealing with UK trade
Separate format errors from registration status in your UX

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.