EU EORI Validation API: SOAP Request and Response
The European Commission validates EORI numbers through one SOAP operation, validateEORI, with no login. It takes up to 10 numbers per request and returns status 0 (valid) or 1 (not valid), plus name and address when the trader consented. It covers the EU and XI, not GB. Avatcado checks EU, XI and GB numbers with one JSON request.
The European Commission checks EORI numbers through one SOAP operation, validateEORI. It needs no account, API key or token. This guide gives the request that works, the response fields, the two ways the service reports a problem, and the limits measured against the live service on 13 November 2026.
Short answer: one SOAP call, no login
Send a SOAP 1.1 envelope with one to ten eori elements to the endpoint below. Each number comes back with status 0 (valid) or 1 (not valid). The trader's name and address are included only when the trader consented to publication. The Commission publishes a WSDL for the service and no developer documentation, so the behavior described here was read from the WSDL and from live responses.
| Endpoint | https://ec.europa.eu/taxation_customs/dds2/eos/validation/services/validation |
|---|---|
| WSDL | The endpoint with ?wsdl appended |
| Operation | validateEORI, namespace http://eori.ws.eos.dds.s/ |
| Authentication | None |
| Covers | Numbers issued by the 27 EU member states, and XI numbers for Northern Ireland |
Send a validateEORI request
The SOAPAction header is an empty quoted string. Numbers must be uppercase with no spaces: the schema pattern is two capital letters followed by at least one character, 17 characters in total at most.
curl --max-time 15 \
"https://ec.europa.eu/taxation_customs/dds2/eos/validation/services/validation" \
--header "Content-Type: text/xml; charset=utf-8" \
--header 'SOAPAction: ""' \
--data '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ev="http://eori.ws.eos.dds.s/">
<soapenv:Body>
<ev:validateEORI>
<ev:eori>IT01234567890</ev:eori>
<ev:eori>NL123456789</ev:eori>
</ev:validateEORI>
</soapenv:Body>
</soapenv:Envelope>'Read the response
The response below uses synthetic values in the shape the live service returns. It shows the three outcomes: valid with published details, valid without details, and not valid.
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns0:validateEORIResponse xmlns:ns0="http://eori.ws.eos.dds.s/">
<return>
<requestDate>13/11/2026</requestDate>
<result>
<eori>IT01234567890</eori>
<status>0</status>
<statusDescr>Valid</statusDescr>
<name>EXAMPLE S.P.A.</name>
<street>VIA ROMA 1</street>
<postalCode>41122</postalCode>
<city>MODENA</city>
<country>Italy</country>
</result>
<result>
<eori>NL123456789</eori>
<status>0</status>
<statusDescr>Valid</statusDescr>
</result>
<result>
<eori>DE000000000000000</eori>
<status>1</status>
<statusDescr>Not valid</statusDescr>
</result>
</return>
</ns0:validateEORIResponse>
</S:Body>
</S:Envelope>| Element | Always present | Meaning |
|---|---|---|
requestDate | Yes | Date of the check as dd/mm/yyyy. There is no time and no reference number |
eori | Yes | The number this result belongs to |
status | Yes | 0 valid, 1 not valid |
statusDescr | Yes | Valid or Not valid |
name, street, postalCode, city, country | No | Trader details, present only with the trader's consent to publication |
address | No | A free-text element. For one Greek trader it held a repeat of the name, so build the address from the fields above |
Three details decide whether a parser is correct:
- Match results on the number. A request for seven numbers came back with the valid results first, in a different order than requested.
- Valid without a name is a complete answer. Of three valid numbers checked, one returned a name and address and two returned neither. Treat the trader fields as optional.
countryis an English country name. The live value wasItaly, where a two-letter code might be expected.
Handle the two error shapes
The service reports problems in two different ways, and neither says anything about the number.
| Trigger | HTTP status | Body |
|---|---|---|
| A number that breaks the schema pattern (lowercase, too long, no prefix) | 500 | SOAP fault, faultcode Client |
| More than 10 numbers in one request | 200 | errorDescription: Maximum allowed number of EORI in one request is 10. No results |
| No numbers in the request | 200 | errorDescription: No response for empty request. |
One malformed number fails the whole request. A batch of ten with a single lowercase entry returns this fault and no result for the other nine:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns0:Fault xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>ns0:Client</faultcode>
<faultstring>cvc-pattern-valid: Value 'de123' is not facet-valid with respect to pattern '[A-Z]{2}\p{IsBasicLatin}+' for type 'EORINumber'.</faultstring>
</ns0:Fault>
</S:Body>
</S:Envelope>Check the format before sending, and check for errorDescription before reading results: an HTTP 200 with no result elements is an error. Record a fault, a timeout or a missing result as a check that did not complete. Recording any of them as Not valid marks a real trader as unregistered.
Limits the service states, and the ones it leaves out
- 10 numbers per request. Verified: 10 returns 10 results, 11 returns the error above.
- Request rate. The WSDL says the number of requests in a timeframe is limited and gives no figure. Plan for throttling: bound each request with a timeout and retry with a pause.
- No uptime commitment, status page or change notice. None is published for this service.
- No search by name. The operation takes numbers only.
- No proof of the check. Unlike a VIES consultation number, the response carries only the date. Store the response yourself if you need evidence.
Route GB and XI numbers correctly
The service accepts a GB number without complaint and answers Not valid, even for a number that HMRC lists as valid. That was confirmed with one published GB number checked against both registers on the same day. Send GB numbers to HMRC, as described in the HMRC EORI check API guide. XI numbers belong here: HMRC's API refuses them and names the Commission's service in its error message.
Greek EORI numbers carry the ISO prefix GR, while Greek VAT numbers carry EL. The service lists a Greek trader as valid under GR and answers Not valid for the same digits under EL. If you reuse a VAT number field for both, see EORI number vs VAT number for the differences.
Run the TypeScript example
This Node.js 20+ example checks one number against the right register: GB with HMRC, every other prefix with the Commission's service. It rejects malformed input before sending, bounds each request to 15 seconds, matches the result on the number, and reports faults and timeouts as Unavailable. It needs no dependency and no credentials. The source ends with its run command.
type EoriResult =
| { status: "Valid"; eori: string; register: "EU" | "HMRC"; name: string | null; address: string | null }
| { status: "Invalid"; eori: string; register: "EU" | "HMRC" }
| { status: "Malformed"; eori: string; reason: string }
| { status: "Unavailable"; eori: string; register: "EU" | "HMRC"; reason: string };
const EU_URL = "https://ec.europa.eu/taxation_customs/dds2/eos/validation/services/validation";
const HMRC_URL = "https://api.service.hmrc.gov.uk/customs/eori/lookup/check-multiple-eori";
const TIMEOUT_MS = 15_000;
const XML_ENTITIES: Record<string, string> = { lt: "<", gt: ">", quot: '"', apos: "'", amp: "&" };
function element(xml: string, name: string): string | null {
const match = xml.match(new RegExp("<(?:[\\w.-]+:)?" + name + ">([\\s\\S]*?)</(?:[\\w.-]+:)?" + name + ">"));
if (!match) return null;
const text = match[1].replace(/&(lt|gt|quot|apos|amp);/g, (_, entity: string) => XML_ENTITIES[entity]).trim();
return text === "" ? null : text;
}
async function checkWithEu(eori: string): Promise<EoriResult> {
const envelope =
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ev="http://eori.ws.eos.dds.s/">' +
"<soapenv:Body><ev:validateEORI><ev:eori>" + eori + "</ev:eori></ev:validateEORI></soapenv:Body>" +
"</soapenv:Envelope>";
const response = await fetch(EU_URL, {
method: "POST",
headers: { "Content-Type": "text/xml; charset=utf-8", SOAPAction: '""' },
body: envelope,
signal: AbortSignal.timeout(TIMEOUT_MS),
});
const xml = await response.text();
// A fault is not an answer about the number. It arrives with HTTP 500.
const fault = element(xml, "faultstring");
if (fault) return { status: "Unavailable", eori, register: "EU", reason: fault };
if (!response.ok) return { status: "Unavailable", eori, register: "EU", reason: "HTTP " + response.status };
// Results come back in the service's own order, so match on the number.
const results = xml.match(/<(?:[\w.-]+:)?result>[\s\S]*?<\/(?:[\w.-]+:)?result>/g) ?? [];
const result = results.find((block) => element(block, "eori") === eori);
if (!result) {
const reason = element(xml, "errorDescription") ?? "no result for the number";
return { status: "Unavailable", eori, register: "EU", reason };
}
const status = element(result, "status");
if (status === "1") return { status: "Invalid", eori, register: "EU" };
if (status !== "0") return { status: "Unavailable", eori, register: "EU", reason: "unexpected status " + status };
// Build the address from the structured fields. The free-text address element can repeat the name.
const locality = [element(result, "postalCode"), element(result, "city")].filter(Boolean).join(" ");
const address = [element(result, "street"), locality, element(result, "country")].filter(Boolean).join(", ");
return { status: "Valid", eori, register: "EU", name: element(result, "name"), address: address || null };
}
async function checkWithHmrc(eori: string): Promise<EoriResult> {
const response = await fetch(HMRC_URL, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/vnd.hmrc.1.0+json" },
body: JSON.stringify({ eoris: [eori] }),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
if (!response.ok) return { status: "Unavailable", eori, register: "HMRC", reason: "HTTP " + response.status };
const entries: unknown = await response.json();
const entry = Array.isArray(entries) ? entries.find((candidate) => candidate?.eori === eori) : undefined;
if (typeof entry?.valid !== "boolean") {
return { status: "Unavailable", eori, register: "HMRC", reason: "no result for the number" };
}
if (!entry.valid) return { status: "Invalid", eori, register: "HMRC" };
const details = entry.companyDetails?.address;
const address = [details?.streetAndNumber, details?.cityName, details?.postcode].filter(Boolean).join(", ");
return {
status: "Valid",
eori,
register: "HMRC",
name: entry.companyDetails?.traderName ?? null,
address: address || null,
};
}
export async function checkEori(input: string): Promise<EoriResult> {
// Greek EORI numbers use GR. The EU register answers "not valid" for the VAT prefix EL.
const eori = input.toUpperCase().replace(/[\s.-]/g, "").replace(/^EL(?=\d)/, "GR");
// Both registers answer a malformed number with an error for the whole request,
// so reject it here instead of spending a call.
if (!/^[A-Z]{2}[A-Z0-9]{1,15}$/.test(eori)) {
return { status: "Malformed", eori, reason: "expected two letters followed by 1 to 15 letters or digits" };
}
const isGb = eori.startsWith("GB");
if (isGb && !/^GB[0-9]{12,15}$/.test(eori)) {
return { status: "Malformed", eori, reason: "GB numbers are GB followed by 12 to 15 digits" };
}
const register = isGb ? "HMRC" : "EU";
try {
return isGb ? await checkWithHmrc(eori) : await checkWithEu(eori);
} catch (error) {
// A timeout or network failure says nothing about the number. Never record it as invalid.
const reason = error instanceof Error ? error.message : String(error);
return { status: "Unavailable", eori, register, reason };
}
}
// Run with Node.js 20 or later: npx tsx check-eori.ts GB123456789000
const input = process.argv[2];
if (input) {
checkEori(input).then((result) => console.log(JSON.stringify(result, null, 2)));
}
The EU service directly vs Avatcado
Calling the Commission directly costs nothing and suits a system that already speaks SOAP and only checks EU numbers. Avatcado puts the same register, and HMRC for GB numbers, behind one JSON endpoint.
| Concern | EU service directly | Avatcado |
|---|---|---|
| Protocol | SOAP 1.1, XML in and out | GET with a query parameter, JSON out |
| Coverage | EU 27 and XI. A GB number gets a wrong Not valid | EU 27, XI and GB, routed by prefix |
| Malformed input | SOAP fault for the whole request | 422 invalid_eori_format naming the rule, not counted against the quota |
| Outage | Your own retry and storage | Up to three attempts, then the last stored answer flagged in the metadata, or a 503 that is not counted |
| Repeat checks | Every check calls the service | Cached, with the age visible in the metadata |
| Cost | Free | Free for 500 requests a month, shared with VAT validation |
# Set AVATCADO_API_KEY in this server environment first.
curl --max-time 30 \
"https://api.avatcado.com/v1/eori/validate?eori_number=GB123456789000" \
--header "Authorization: Bearer $AVATCADO_API_KEY"{
"data": {
"valid": true,
"eori_number": "GB123456789000",
"country_code": "GB",
"company": {
"name": "EXAMPLE TRADING LTD",
"address": "1 HIGH STREET, LONDON, SW1A 1AA"
},
"requested_at": "2026-11-13T09:19:29.000Z"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"request_duration_ms": 312,
"source": "hmrc",
"source_status": "live",
"cached": false,
"stale": false
}
}company is null when the register publishes no details for a valid number. The same API key validates VAT numbers in 32 countries; an EORI check counts as one request.
Frequently asked questions
Is there an official EORI validation API for the EU?
Yes. The European Commission runs a SOAP web service with one operation, validateEORI, at ec.europa.eu/taxation_customs/dds2/eos/validation/services/validation. It needs no account or key. The Commission publishes the WSDL and no developer documentation, and it offers no REST or JSON version.
How many EORI numbers can one request validate?
Ten. On 13 November 2026 a request with 10 numbers returned 10 results, and a request with 11 returned HTTP 200 with an error description saying the maximum allowed number of EORI in one request is 10, and no results.
Why does a valid EORI number come back without a company name?
The register shows a trader's name and address only when the trader consented to publication. A result with status 0 and no name is a complete answer: the number is registered and no details are public. Of three valid numbers checked for this guide, two came back without a name.
Can the EU service validate GB or XI EORI numbers?
XI numbers, yes. GB numbers, no: the service answers Not valid for a GB number that HMRC lists as valid, so a GB number must be checked with HMRC's Check an EORI number API. Avatcado routes each number to the right register by its prefix.
Sources
- EORI validation service WSDL European Commission, accessed November 13, 2026
- EORI number validation (online tool) European Commission, accessed November 13, 2026
- Economic Operators Registration and Identification number (EORI) European Commission, accessed November 13, 2026
- Finding and checking an EORI GOV.UK, accessed November 13, 2026
Related guides
HMRC EORI Check API: Request, Response and Errors
Check GB EORI numbers with HMRC's open API: the POST request, response fields, the four 400 messages, the 10-number limit and where XI numbers go.
EORI Number vs VAT Number: What Each One Proves
An EORI number is a customs registration and a VAT number is a tax registration. See how they differ, why one does not prove the other, and where to check each.
VIES API Guide: REST Endpoint, SOAP & Integration
Use the official VIES REST API from Node.js, handle HTTP 200 faults, preserve consultation evidence, compare SOAP, and plan for member-state downtime.
HMRC VAT Check API: OAuth, Examples & Error Handling
Integrate HMRC's Check a UK VAT Number API with OAuth client credentials, Node.js examples, verified lookups, and structured error handling.