# EU VAT Number Validator — VIES, Bulk, With Company Name (`dalbian/eu-vat-number-validator-vies`) Actor

Validate EU VAT numbers in bulk against the European Commission's VIES service. Returns the registered company name and address where the member state discloses them, a consultation number that is legal proof of the check, and an optional trader name and address match. Checks the national.

- **URL**: https://apify.com/dalbian/eu-vat-number-validator-vies.md
- **Developed by:** [Benjamin Jerez](https://apify.com/dalbian) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## EU VAT Number Validator — VIES, Bulk, With Company Name and Address

Validate EU VAT numbers in bulk against **VIES**, the European Commission's front end to the 27 national VAT registers plus Northern Ireland. For each number you get validity, the registered company name and address where the member state discloses them, the consultation number that is your legal record of the check, and an optional match of the company details you hold against the register.

No API key. No scraping. The Commission's own REST service, queried live.

### What a check returns

`IT00159560366`:

| Field | Value |
| --- | --- |
| `countryCode`, `vatNumber` | IT · 00159560366 |
| `formatValid`, `checksumChecked`, `checksumValid` | true · true · true |
| `viesChecked`, `valid`, `status` | true · true · VALID |
| `companyName` | FERRARI S.P.A. |
| `companyAddress` | VIA EMILIA EST 1163 41122 MODENA MO |
| `requestDate` | 2026-09-18T13:12:45.845Z |
| `requestIdentifier` | WAPIAAAAaC0peNOd |

The same run also returned `SA SODIMAS` for `FR40303265045`, `NV Anheuser-Busch InBev` in Leuven for `BE0417497106`, `ORLEN SPÓŁKA AKCYJNA` for `PL7740001454`, `Carlsberg A/S` for `DK61056416`, `Telefonaktiebolaget LM Ericsson` for `SE556016068001`, `Hrvatski Telekom d.d.` for `HR81793146560`, `ČEZ, a. s.` for `CZ45274649` and `ΟΡΓΑΝΙΣΜΟΣ ΤΗΛΕΠΙΚΟΙΝΩΝΙΩΝ ΤΗΣ ΕΛΛΑΔΟΣ` for `EL094019245`.

A `summary` row comes first with the counts, the number of VIES calls made, and the availability of every national database at the moment of the run.

### Which countries release the company name

This surprises people, so the Actor states it on the row rather than leaving a blank. From live runs against all 27 member states:

**Name and address returned:** AT, BE, BG, CY, CZ, DK, EE, EL, FI, FR, HR, HU, IE, IT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK.

**Name only:** **LT**, which returns the company name and the literal `N/A` in place of the address.

**Nothing returned, by policy:** **DE** and **ES**. Both answer `---` for every trader. A German number is valid or it is not, and that is all you get.

When that happens the row carries `detailsDisclosed: false` and an explanation saying the field is empty by policy, not because the record is thin. The three different sentinels member states use for "no value" — `---`, an empty string and `N/A` — are all normalised to null, so nothing downstream ever has to treat `N/A` as a company name.

### Verifying the company details you already hold

This is the part an auditor wants, and it is the reason the non-disclosing countries are not a dead end.

Supply `traderDetails` alongside the numbers and VIES compares each field against the register and answers `VALID` or `INVALID` per field — **without ever revealing the registered value**. So you can prove that the VAT number on an invoice belongs to the company named on that invoice, even in Spain, where the name is never disclosed.

From the test run, `ESA28015865` with `{"name": "TELEFONICA SA", "city": "MADRID"}`:

```
traderNameMatch:  VALID
traderCityMatch:  VALID
companyName:      null        (Spain does not disclose)
requestIdentifier: bb948748-c43f-4724-9421-2aee5672709b
```

And `ESA15022510` with a deliberately wrong name but the right address:

```
traderNameMatch:        INVALID
traderPostalCodeMatch:  VALID
traderCityMatch:        VALID
```

**Only some member states run this comparison.** In testing, Spain processed it and returned real `VALID`/`INVALID` flags. France, Italy, Belgium, Poland, Luxembourg, Denmark, Ireland and Germany all answered `NOT_PROCESSED` for every field. When that happens the Actor sets `matchProcessed: false` and says which country declined, rather than leaving five empty columns that look like failures. The VAT number itself is still checked normally.

The match needs `requesterVatNumber` — your own valid EU VAT number. Without it VIES ignores the trader fields silently.

### The consultation number, and why it matters

`requestIdentifier` is the reference VIES issues for a consultation. Under EU VAT rules the supplier carries the burden of proving that a zero-rated intra-Community supply went to a genuinely VAT-registered customer. A screenshot is weak evidence; the consultation number is the reference a tax authority can look up in the Commission's own logs to confirm that you checked, what you asked, and on which date.

**It is only issued when you supply `requesterVatNumber`.** Confirmed directly: the same request to `FR40303265045` returned `requestIdentifier: ""` without requester details and `WAPIAAAAaC0iRI3o` with them. If you are keeping evidence for an audit, fill that field in — it costs nothing and it is the whole point.

The format varies by member state. Most return a sixteen-character token like `WAPIAAAAaC0peNOd`; Spain, Sweden and Austria return a UUID such as `bb948748-c43f-4724-9421-2aee5672709b`. Both are equally valid; store whatever comes back.

### An outage is not an invalid number

Each national VAT database can go down independently while the rest of VIES stays up. VIES reports this **inside an HTTP 200 response**, in an envelope that looks nothing like a normal answer:

```json
{"actionSucceed": false, "errorWrappers": [{"error": "MS_UNAVAILABLE"}]}
```

A validator that parses only the happy path reads no `valid: true` and writes `valid: false`. Every customer in that country is then marked as having a bad VAT number, on a day when nothing was wrong with any of them. That is the single most damaging thing this kind of tool can do, and it is why the Actor never sets `valid` from anything other than an answer that VIES actually gave.

Each failure mode gets its own `status` and a plain explanation, and `valid` stays null:

| VIES error | `status` | Retried |
| --- | --- | --- |
| `MS_UNAVAILABLE` | `UNAVAILABLE` | yes |
| `SERVICE_UNAVAILABLE` | `UNAVAILABLE` | yes |
| `TIMEOUT` | `TIMEOUT` | yes |
| `MS_MAX_CONCURRENT_REQ` | `RATE_LIMITED` | yes |
| `GLOBAL_MAX_CONCURRENT_REQ` | `RATE_LIMITED` | yes |
| `INVALID_INPUT` | `INVALID_INPUT` | no |
| `INVALID_REQUESTER_INFO` | `INVALID_REQUESTER` | no |
| `VOW-ERR-*` | `REJECTED` | no |

With `checkServiceStatusFirst` on, the Actor reads `check-status` before it starts, records the availability of all 28 databases on the summary row, and does not spend a request on a country already reported down — those rows come back `UNAVAILABLE` with the country named.

### The format and check-digit test, before any network call

Every number is checked against its national format and check digit locally first. A typo is caught for free, and VIES — which is slow and rate-limited — is not asked a question that has only one possible answer.

Two different judgements, kept apart on purpose:

- **`formatValid`** — is the length and character set right for that country? A `false` here is certain, so the VIES call is skipped (`skipFormatInvalid`, on by default).
- **`checksumValid`** — does it satisfy the national check-digit rule? A `false` here is very probably a typo but not a proof, so by default the number is **still sent to VIES**. VIES is the authority; a local algorithm is not.

That asymmetry is deliberate. A check-digit rule that is slightly wrong, applied as a hard filter, quietly deletes real customers from your results. You can turn on `skipChecksumInvalid` if you would rather have the speed.

All 27 member states plus XI have a format and length rule. Check digits are implemented for **all 27 member states**, and each one was verified against real VAT numbers that VIES itself confirmed valid — never against a published example.

Where a country issues numbers under several different schemes that cannot be told apart from the number alone, the Actor reports `checksumChecked: false` instead of guessing: Bulgarian and Czech personal identifiers, Latvian natural-person codes, pre-2013 Irish numbers, French keys containing letters, and Northern Irish `GD`/`HA` government numbers.

Three rules are allowed to **confirm** a number but never to reject one, because they could not be verified to the same standard: the Dutch *elfproef*, the nine-digit Lithuanian branch, and Northern Ireland.

The Dutch case is the instructive one. Sole traders registered since 1 January 2020 carry a randomly generated number that does not satisfy the old *elfproef*, and they are entirely valid. So a passing Dutch check digit is reported as a pass and a failing one as **not checked** rather than as a wrong number. Erring the other way would reject every Dutch one-person business in a list.

Across **94 VAT numbers that VIES confirmed valid, in all 27 member states**, the local rules produced **zero** false rejections, while catching 98.4% of randomly injected single-digit typos.

### Input formats

Anything readable is accepted. `DE811907980`, `DE 811 907 980`, `de-811.907.980` and `DE811907980 ` are the same number: spaces, dots and hyphens are stripped and the country code is uppercased.

Greece is the classic trap — the ISO country code is **GR** but the VAT prefix is **EL**, and VIES rejects `GR` outright with `INVALID_INPUT`. Both are accepted here and sent as `EL`.

Great Britain is no longer in VIES. A `GB` number comes back with `status: NOT_IN_VIES` and a note pointing at HMRC, rather than the bare `INVALID_INPUT` that VIES itself returns. Northern Ireland is still served, under the prefix `XI`.

### Speed and throughput

VIES enforces a concurrency limit per member state and rejects excess traffic with `MS_MAX_CONCURRENT_REQ`. This is easy to trigger: during development, six parallel requests were enough.

The Actor therefore uses a semaphore of 2 by default and spaces requests roughly 0.6 seconds apart. Measured on a real run: **28 numbers across 18 countries, 26 VIES calls, 32.0 seconds** — about 1.4 seconds per number that reached the network. Expect roughly **2 000 to 2 500 numbers an hour** on a clean list, and materially less when one of the national databases is having a slow day. Raising `concurrency` above 2 or 3 mostly buys retries rather than throughput, because the per-country limit is the binding constraint.

Format-rejected numbers cost nothing and take no time, so a dirty list runs faster than a clean one of the same length.

### Who uses this

Finance and accounts-payable teams validating supplier VAT numbers before a payment run. E-commerce and SaaS billing systems deciding whether to zero-rate a B2B sale under the reverse charge. Marketplaces onboarding EU sellers. Auditors and tax advisers who need the consultation number on file. CRM and data teams cleaning a customer list that has accumulated typos and dissolved companies for years.

### Scope and limits

**VIES answers "registered for intra-EU trade", not "exists".** A company can be VAT-registered domestically and still return invalid here, because it is not registered for intra-Community transactions. Germany in particular issues a separate `USt-IdNr.` for that purpose, which is the number VIES knows.

**The name and address are the register's, not the company's letterhead.** They are frequently abbreviated, in the local script and occasionally out of date — `SE556016068001` returns `Telefonaktiebolaget LM Ericsson`, not `Ericsson`. Match on the VAT number, not on the name.

**A valid answer is a snapshot.** Registrations are withdrawn. The `requestDate` on each row is the moment the answer was given, and it is what a tax authority will care about.

**VIES is a relay, not a database.** It forwards each query to the member state that issued the number. Availability, response time and how much detail comes back are decided by that national administration, not by the Commission.

### Source and licence

**VIES** — the VAT Information Exchange System of the European Commission's Directorate-General for Taxation and Customs Union, through `ec.europa.eu/taxation_customs/vies/rest-api`. Free, keyless, no registration. Queried live on every run; nothing is cached.

### Frequently asked

**Why is `companyName` empty for a valid German or Spanish number?**
Because those two member states do not release trader details through VIES at all. The row says so in `explanation` and sets `detailsDisclosed: false`. For Spain you can still verify the name you hold with the trader match; for Germany you cannot.

**Why did all my match flags come back empty?**
Either the member state does not run the comparison — most do not, and the row says which — or `requesterVatNumber` was missing, in which case VIES ignores the trader fields without complaining.

**A number I know is good came back invalid. Why?**
Most often because the company is registered domestically but not for intra-EU trade, which is a different register. Check `requestDate` and `status`: if `status` is `VALID` or `INVALID` the national database genuinely answered, and the answer is the member state's, not the Actor's.

**Can I check a UK VAT number?**
Not through VIES. Great Britain left the EU VAT area on 31 December 2020. Northern Ireland numbers work under the prefix `XI`.

**How many numbers can I put in one run?**
As many as you like. The constraint is VIES throughput, not the Actor — budget about half an hour for a thousand.

**Does a format-rejected number cost a VIES call?**
No. It never leaves the machine.

# Actor input Schema

## `vatNumbers` (type: `array`):

One VAT number per line, in any form: DE811907980, DE 811 907 980, de-811.907.980. Spaces, dots and hyphens are stripped and the country code is uppercased. Greece may be written either GR or EL. Northern Ireland is XI.

## `traderDetails` (type: `array`):

Switches on the trader match. One object per VAT number, in the same order as the list above, with any of name, street, postalCode, city, companyType — for example \[{"name": "TELEFONICA SA", "city": "MADRID"}]. VIES answers VALID or INVALID per field without ever revealing the registered value. Use an empty object {} for a number you do not want matched. Requires the requester VAT number below.

## `requesterVatNumber` (type: `string`):

Your company's own valid EU VAT number, for example IT00159560366. Supplying it makes VIES return a requestIdentifier — the consultation number you keep as evidence that you checked. It is also required for the trader match. Leave empty for an anonymous check.

## `checkServiceStatusFirst` (type: `boolean`):

Ask VIES which national databases are up before validating, and record the answer on the summary row. Costs one request and tells you in advance whether a country in your batch is reachable.

## `skipFormatInvalid` (type: `boolean`):

Do not spend a VIES call on a number whose length or characters are wrong for its country — those can never be valid. Numbers that fail only the check digit are still sent to VIES, because a check-digit rule can have exceptions and VIES is the authority.

## `skipChecksumInvalid` (type: `boolean`):

Faster and cheaper on a dirty list, but riskier: a number that fails the check digit is almost always a typo, yet the Actor will not have asked VIES to confirm it. Off by default.

## `concurrency` (type: `integer`):

VIES enforces a concurrency limit per member state and rejects excess traffic with MS\_MAX\_CONCURRENT\_REQ. Two is the setting that finishes fastest in practice; raising it mostly buys retries.

## Actor input object example

```json
{
  "vatNumbers": [
    "IE6388047V",
    "DE811907980"
  ],
  "checkServiceStatusFirst": true,
  "skipFormatInvalid": true,
  "skipChecksumInvalid": false,
  "concurrency": 2
}
```

# Actor output Schema

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "vatNumbers": [
        "IE6388047V",
        "DE811907980"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dalbian/eu-vat-number-validator-vies").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "vatNumbers": [
        "IE6388047V",
        "DE811907980",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dalbian/eu-vat-number-validator-vies").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "vatNumbers": [
    "IE6388047V",
    "DE811907980"
  ]
}' |
apify call dalbian/eu-vat-number-validator-vies --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dalbian/eu-vat-number-validator-vies"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/E011w4WoT4bRMBt0N/builds/Q3DFgLKfTTB5kzIzo/openapi.json
