# VIES VAT Validator – Bulk EU VAT Number Check (`lwsdjfls/vies-vat-validator`) Actor

Bulk VAT number validation through VIES for invoicing software, accountants and B2B webshops. Tells valid, invalid and "member state down" apart (never a false invalid), returns the registered name and the consultation number for reverse charge, and finds VAT numbers by company name.

- **URL**: https://apify.com/lwsdjfls/vies-vat-validator.md
- **Developed by:** [Leyten Data](https://apify.com/lwsdjfls) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 vat number validateds

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

### What does VIES VAT Validator do?

**Bulk VAT number validation through VIES** for invoicing software, accountants and B2B webshops: per EU VAT number (btw-nummer) it tells you whether the number is valid, invalid, or could not be checked because the member state's service did not answer. It returns the registered name and address where the member state discloses them, the VIES consultation number you need as evidence for reverse charge, and it can find the VAT number of a company by name.

#### The problem: "invalid" and "the service was down" look the same

VIES is not one database. It relays every check to the tax administration of the member state, and those services fail independently:

- **Germany** takes its VIES service offline every night (measured in August 2026: roughly 21:40 to 01:00 UTC, every night, weekends included).
- **Ireland** was unavailable every Monday night, and **Latvia** degrades during the day.
- Member states answer `MS_MAX_CONCURRENT_REQ` when they are busy. On 15 September 2026 France answered it to every check for minutes, while the VIES status endpoint listed France as "Available".
- The endpoint most tools copy from the VIES website returns `"isValid": false` together with an error code when the check did not happen at all.

A validator that turns any of these into "invalid" breaks the accounting integration behind it: a customer with a perfectly valid VAT number is suddenly charged VAT, blocked at checkout, or flagged in your ICP/EC Sales List review. This Actor treats that distinction as the product.

#### How outages are handled

Per member state, the Actor keeps a gate with its own concurrency (at most 3 checks at a time), backoff and failure clock. It only gives up on a member state after it kept failing for **Wait for an unavailable member state** (default 5 minutes); from then on its remaining numbers in the run are reported as `unavailable` immediately. When VIES itself reports the member state as unavailable, it checks once a minute instead of hammering a service that is down.

| VIES error | Meaning | What the Actor does |
| --- | --- | --- |
| `MS_UNAVAILABLE` | The member state's service is not replying | Retried with growing waits (5 s up to 1 min, calmer while VIES reports the state as down) until the maximum wait, then `unavailable` |
| `TIMEOUT` | The member state did not reply in time | Retried with growing waits (5 s up to 1 min, calmer while VIES reports the state as down) until the maximum wait, then `unavailable` |
| `MS_MAX_CONCURRENT_REQ` | Too many requests to this member state right now | One request at a time for that member state, retried from 1 s up to 20 s, then `unavailable` |
| `MS_MAX_CONCURRENT_REQ_TIME` | Too many requests to this member state in a period | One request at a time for that member state, retried from 1 s up to 20 s, then `unavailable` |
| `GLOBAL_MAX_CONCURRENT_REQ` | Too many requests to VIES right now | All checks pause and retry from 2 s up to 30 s, then `unavailable` |
| `GLOBAL_MAX_CONCURRENT_REQ_TIME` | Too many requests to VIES in a period | All checks pause and retry from 2 s up to 30 s, then `unavailable` |
| `SERVICE_UNAVAILABLE` | VIES itself (network or application) is failing | All checks pause and retry from 5 s up to 1 min, then `unavailable` |
| `VAT_BLOCKED` | VIES temporarily refuses checks of this number | `unavailable` for this number; retried in the next run |
| `IP_BLOCKED` | VIES refuses requests from this IP address | All remaining numbers `unavailable`; retried in the next run |
| `INVALID_REQUESTER_INFO` | Your own VAT number was rejected | The run stops before anything is charged |
| `INVALID_INPUT` | Country code or number format rejected | `invalid-format`, not charged |

Unknown error codes are treated as an outage, never as an answer. A run in which VIES only answers in an unexpected way (for example after an API change) fails with a clear message instead of reporting every number as unavailable.

**Next runs.** In a saved task (or a run with a **State key**), unavailable numbers and company names are checked again in the following runs for up to **Keep retrying for** days (default 7) after the first failed check, and delivered as `changed` with `carriedOver: true` once VIES answers. A number that is still unavailable, or that the run's check limit, your spending limit or the run's memory left unchecked, does not produce a new record and keeps waiting.

#### Find a VAT number by company name

VIES cannot search by name. For names in **Company names** (`"Coolblue; NL"`), the Actor finds candidates and lets VIES decide:

1. **Candidates** come from the [Peppol Directory](https://directory.peppol.eu) in every country (company identifiers such as a Belgian enterprise number, French SIREN, Danish CVR or a VAT number determine the VAT number), and for France also from the French government's company register (recherche-entreprises).
2. **Similarity.** Names are compared without legal forms, punctuation, case or accents (`Coolblue B.V.` equals `COOLBLUE BV`: one record, and another of these spellings in a later run is no change), tolerating typos. The best candidates are validated in VIES; Spain also compares the name inside VIES.
3. **Result.** The best valid candidate above **Minimum name similarity** becomes the result, with `match.score`, `match.source` and up to five `match.alternatives`. When two valid VAT numbers match equally well (`Coolblue B.V.` and `Coolblue GmbH` for "Coolblue"), `match.ambiguous` is `true` and the reason names both.

Coverage is honest, not complete: for France it covers all active companies; elsewhere it covers companies registered on Peppol, which includes most Belgian businesses since Belgium made Peppol e-invoicing mandatory in January 2026, but only a minority of Dutch or German ones. French sole traders are skipped in the name search (see the GDPR question below); validate their VAT number directly.

### Features

- **Bulk EU VAT number check** against VIES, thousands of numbers per run, with one record per distinct number
- **`invalid` only when VIES answered.** Every error, time-out and throttle becomes `unavailable`, with the VIES error code, the attempts made and, for known maintenance windows, when to retry
- **Retries per member state.** A German outage does not slow down Dutch numbers; each member state gets its own concurrency, backoff and maximum wait
- **Unavailable numbers come back.** In a saved task, numbers VIES could not check are checked again in the next runs until VIES answers (for up to a week by default), even if your next batch no longer contains them
- **A second opinion.** An "invalid" answer from a member state that failed during the same run is checked once more before it is reported
- **VIES consultation number** per check when you provide your own VAT number: proof for reverse charge invoices
- **VAT number lookup by company name** through the Peppol Directory and the French company register, confirmed in VIES
- **Never charged for downtime.** Only definite answers cost money
- **Change detection** per number across runs (`added`, `unchanged`, `changed`) with a webhook, so a customer who is no longer valid shows up the day it happens
- Runs through the Apify API, on a schedule, or from Make, Zapier and n8n

### Use cases

#### ERP and invoicing software: validate customer VAT numbers in bulk

Call the Actor from your backend when customers are created or imported, or nightly for the whole customer file. Branch on `status`, not on `valid` alone, and never store a record with `fromEarlierRun: true` as a new check:

```js
// npm install apify-client
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

// customers: [{ id: 'C-1001', vatNumber: 'NL810433941B01' }, ...]
// One run checks as many numbers as its memory holds (15,000 with the default 1 GB, 35,000 with 2 GB, 50,000 with 4 GB).
// A larger file at 1 GB leaves the same customers unchecked every night, so pick the memory by the size of the file.
const memory = customers.length <= 15_000 ? 1024 : customers.length <= 35_000 ? 2048 : 4096;
const run = await client.actor('lwsdjfls/vies-vat-validator').call(
    {
        vatNumbers: customers.map((customer) => customer.vatNumber),
        requesterVatNumber: 'NL123456789B01', // your own VAT number: VIES returns consultation numbers
        maxWaitMinutes: 10,
        maxChecksPerRun: customers.length, // the default of 10,000 would stop a larger file
    },
    { memory },
);
// Runs without a task or state key share one state and must not overlap: a run started while another is still
// working fails before it checks or charges anything. Queue the calls, or start the run again later.
if (run.status !== 'SUCCEEDED') throw new Error(`VAT check run ${run.id} ended with status ${run.status}`);
// A run that its check limit, your spending limit or its memory stopped still succeeds: its RUN_SUMMARY says "partial",
// and its log says what was left out and what makes room. Watch for it, or the customers left out wait every night.
const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('RUN_SUMMARY');
if (summary?.value.outcome === 'partial') console.warn(`VAT check run ${run.id} did not check every number; see its log`);

const { items } = await client.dataset(run.defaultDatasetId).listItems({ clean: true });
// One record per distinct number: spellings of one number ("NL 8104.33.941.B01", "nl810433941b01") are checked
// once, so look records up by the number as the Actor normalizes it.
const normalize = (vatNumber) => vatNumber.replace(/[\s.\-/,_:]/g, '').toUpperCase().replace(/^GR/, 'EL').replace(/^BE(\d{9})$/, 'BE0$1');
const byNumber = new Map(items.map(({ payload }) => [payload.vatNumber ?? normalize(payload.input), payload]));

for (const customer of customers) {
    const result = byNumber.get(normalize(customer.vatNumber));
    // Not checked in this run (your spending limit was reached, or the run's memory was full): keep the evidence you
    // have and check again later. A record with fromEarlierRun repeats an earlier result; it is not a new check.
    if (!result || result.status === 'skipped' || result.fromEarlierRun) {
        await scheduleRecheck(customer.id, null);
        continue;
    }
    switch (result.status) {
        case 'valid': // reverse charge allowed; keep the evidence with the customer
            await saveVatCheck(customer.id, { reverseCharge: true, name: result.name, consultationNumber: result.consultationNumber, checkedAt: result.checkedAt });
            break;
        case 'invalid':
        case 'invalid-format': // charge VAT and ask the customer for a correct number
            await saveVatCheck(customer.id, { reverseCharge: false, reason: result.statusReason });
            break;
        case 'unavailable': // no answer: keep the last known status and check again later
            await scheduleRecheck(customer.id, result.unavailable.retryAfter);
            break;
        default: // outside-vies (GB, CH, ...): handle outside the EU flow
            await flagForReview(customer.id, result.statusReason);
    }
}
```

1,000 customers cost $4.00 when VIES answers all of them; numbers VIES could not check cost nothing.

#### Accountants: monthly check of intra-EU customers before the ICP/EC Sales List

Save a task with the customer VAT numbers of your client, schedule it monthly (or before each filing), and add your client's VAT number for consultation numbers. Each run lists every number; the `changeType` column shows which customers changed since last month, and a webhook can alert you when a customer is **no longer valid**. A record with `fromEarlierRun: true` was not checked again because the check limit or your spending limit was reached: its `unchanged` says nothing about this month.

#### E-commerce with EU B2B sales

Validate the VAT numbers entered at checkout in batches (for example every hour) instead of in the checkout itself, so a German night-time outage never blocks an order: accept the order, and invoice with reverse charge once the number is `valid`.

#### Tax teams: reverse charge validation with proof

For an intra-EU B2B sale you apply the 0% rate or reverse charge (verlegde btw, Steuerschuldnerschaft des Leistungsempfängers, autoliquidation) only for a customer with a valid VAT number:

- **Goods.** Since the 2020 Quick Fixes, the customer's VAT number being valid in VIES is a substantive condition for the exemption of an intra-Community supply (Article 138 of the VAT Directive), and the number must appear in your recapitulative statement.
- **Services.** You may treat the customer as a taxable person when it gave you its VAT number and you verified it (Article 18 of Implementing Regulation 282/2011).

Checking is half of it; being able to show you checked is the other half. Put your own VAT number in **Your VAT number** and VIES issues a **consultation number** for every check: a reference, stored by VIES, that you verified that customer's number on that date. The Actor returns it with the check timestamp, so you can store it with the invoice or customer record.

### How to use it

1. Paste your VAT numbers into **VAT numbers**, one per line, and press Start. A country prefix is all that is needed; spaces, dots and dashes are ignored, and a number without a prefix uses **Default country**. Every number comes back as its own record.
2. Fill in **Your VAT number** if you need evidence for reverse charge. VIES then issues a consultation number for each check and it lands in the record next to the answer, which is what an auditor asks for.
3. Save the run as a task once the input is right, then call that task from your invoicing system. A task keeps its own state, so later runs report only what changed, and numbers VIES could not reach are checked again for you over the following week.

Three dataset views tell you where to look. `validation` holds the answer per number, `unavailable` lists the numbers a member state never answered for, and `companyMatches` shows which VAT number a company name resolved to and how confident that match is. Every run ends with a status message counting all three, so a quiet day never looks like a broken integration. Read one record in the Output tab before you wire anything up.

Two things this Actor deliberately does not do. It never turns an outage into `invalid`, so a number you get back as invalid really was rejected by the member state. And it does not check that a name belongs to a number: VIES returns the registered name where the member state discloses it, and comparing that with your own customer record stays your decision.

```bash
curl "https://api.apify.com/v2/acts/lwsdjfls~vies-vat-validator/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"vatNumbers":["NL810433941B01","BE0403170701"],"requesterVatNumber":"NL123456789B01"}'
```

### Input

| Field | What it does | Example |
| --- | --- | --- |
| `vatNumbers` | VAT numbers, one per line, with country prefix. Spaces, dots and dashes are fine; GR is read as EL. Numbers without a prefix use the default country. | `["NL810433941B01", "BE 0403.170.701"]` |
| `companyNames` | Names as "Company name; country code". Candidates come from the Peppol Directory and, for France, the French company register; each is confirmed in VIES. | `["Coolblue; NL", "Danone; FR"]` |
| `defaultCountry` | Country for VAT numbers without prefix and names without country code. | `NL` |
| `requesterVatNumber` | Your own EU VAT number. VIES then issues a consultation number per check as proof of verification. | `NL123456789B01` |
| `maxWaitMinutes` | How long a member state may keep failing before its remaining numbers are reported as unavailable. Waiting is free. | `5` |
| `retryUnavailableInLaterRuns` | In saved tasks: numbers VIES could not check are checked again in the following runs and delivered once VIES answers. | `true` |
| `retryForDays` | How many days an unavailable number is retried after the first failed check. | `7` |
| `minMatchScore` | How similar a registered name must be to the name you gave (legal forms, punctuation and case ignored; 100 = identical). | `80` |
| `maxCandidatesPerCompany` | How many similar companies are validated in VIES per name. | `3` |
| `maxChecksPerRun` | Caps the charged validations and matches in one run. Inputs above it are not checked: a number with an earlier result keeps it (with `fromEarlierRun: true`), others are listed as skipped. | `10000` |
| `maxConcurrency` | Checks running at the same time (at most 3 per member state). | `6` |
| `useTestService` | Free rehearsal against the official VIES test service (see below). | `false` |
| `webhookUrl` | Receives a POST with the new and changed results after each run; stored encrypted. | `https://hooks.example.com/vat-checks` |
| `stateKey` | Keeps results and retries separate for runs outside a saved task. Runs without a task or state key share one state. | `customer-file-2026` |
| `proxyConfiguration` | Not needed normally; only when VIES answers IP\_BLOCKED. | `{ "useApifyProxy": false }` |

```json
{
  "vatNumbers": ["NL810433941B01", "BE 0403.170.701", "DE143454214", "FR27552032534"],
  "companyNames": ["Coolblue; NL", "Danone; FR"],
  "requesterVatNumber": "NL123456789B01",
  "maxWaitMinutes": 5,
  "webhookUrl": "https://hooks.example.com/vat-checks"
}
```

**Batch size and memory.** One run checks up to 15,000 numbers and company names with the default 1 GB of memory, 35,000 with 2 GB and 55,000 (the input maximum) with 4 GB; 5,000 with 512 MB. Results kept from earlier runs of the same task or state key take memory too: every 4 that are not in the run take the place of one number. What does not fit is not checked, not charged and not delivered: the run ends as `partial` in its run summary, the log says what was left out and what would make room for it, and numbers VIES could not check in earlier runs wait for a later run. A run in which nothing fits checks and charges nothing. When results whose number was last checked more than 35 days ago take the room, it deletes them and ends without records, so that the next run has room (numbers and company names of its input among them then arrive as `added` again, not compared with their deleted results); otherwise it fails, and its message says what makes room and from when runs make room again on their own.

**A state that keeps getting new numbers fills up.** A result stays until a run of its task or state key starts more than 35 days after the number was last checked. A task, or the one state of all runs without a task or state key, that checks new numbers all the time (hourly checkout batches, an API call for every new customer) therefore holds about 60,000 distinct numbers per 35 days with 1 GB of memory, 20,000 with 512 MB, 140,000 with 2 GB and 270,000 with 4 GB. The fuller the state, the fewer new numbers a run checks; once it is full, runs fail, and a failed run deletes nothing, so runs keep failing until you act or until enough results are more than 35 days old (the failure message gives the date): start these runs with more memory, give them a state key that you change regularly (`checkout-2026-09`, then `checkout-2026-10`), or delete the results of the full state as the GDPR question below describes. A new state key starts over: every number arrives as `added` again, numbers VIES could not check are retried only under their old key, and the results of the old key stay until you delete them. Delete the records of the old key (keys starting with `checkout-2026-09.`) once you no longer need them.

**Runs that overlap.** Every task and every state key has its own state, and all runs without a task or state key share one. Two runs with the same state must not work at the same time: a run that starts while another one with its state is still working fails at once, before it checks or charges anything. Queue calls that can overlap, start a failed run again later, or give each integration its own state key (its unavailable numbers are then retried in its later runs).

**Testing your integration.** Set **Use the VIES test service** to rehearse every situation for free with the official VIES test numbers: `100` valid, `200` invalid, `300` SERVICE\_UNAVAILABLE, `301` MS\_UNAVAILABLE, `302` TIMEOUT, `400` VAT\_BLOCKED, `600` MS\_MAX\_CONCURRENT\_REQ (with a default country).

#### Supported member states

All 27 EU member states and Northern Ireland (`XI`, for trade in goods):

| Prefix | Member state | Example |
| --- | --- | --- |
| `AT` | Austria | `ATU12345678` |
| `BE` | Belgium | `BE0403170701` |
| `BG` | Bulgaria | `BG123456789` |
| `CY` | Cyprus | `CY12345678X` |
| `CZ` | Czechia | `CZ12345678` |
| `DE` | Germany | `DE123456789` |
| `DK` | Denmark | `DK12345678` |
| `EE` | Estonia | `EE123456789` |
| `EL` | Greece | `EL123456789` |
| `ES` | Spain | `ESB12345678` |
| `FI` | Finland | `FI12345678` |
| `FR` | France | `FR27552032534` |
| `HR` | Croatia | `HR12345678901` |
| `HU` | Hungary | `HU12345678` |
| `IE` | Ireland | `IE9700053D` |
| `IT` | Italy | `IT12345678901` |
| `LT` | Lithuania | `LT123456789` |
| `LU` | Luxembourg | `LU12345678` |
| `LV` | Latvia | `LV12345678901` |
| `MT` | Malta | `MT12345678` |
| `NL` | Netherlands | `NL810433941B01` |
| `PL` | Poland | `PL1234567890` |
| `PT` | Portugal | `PT123456789` |
| `RO` | Romania | `RO12345678` |
| `SE` | Sweden | `SE123456789001` |
| `SI` | Slovenia | `SI12345678` |
| `SK` | Slovakia | `SK1234567890` |
| `XI` | Northern Ireland | `XI123456789` |

Formats are checked structurally (length and characters). Check digits are deliberately left to VIES: a local check-digit rule that is wrong for one class of numbers would report real customers as invalid (Dutch VAT numbers issued to sole traders since 2020 no longer pass the old check-digit test, for example).

Numbers from these countries are reported as `outside-vies`:

- `GB`: Great Britain left VIES in 2021 (HMRC checks GB numbers; businesses in Northern Ireland trading goods use XI).
- `CH`: Switzerland is not in VIES (the Swiss UID register checks CHE numbers).
- `NO`: Norway is not in VIES (the Brønnøysund register checks Norwegian numbers).
- `IS`: Iceland is not in VIES.
- `LI`: Liechtenstein is not in VIES.

### Output

One record per distinct VAT number or company name that fits in the run's memory (see **Batch size and memory** under Input):

- `status`: `valid`, `invalid`, `invalid-format`, `outside-vies`, `unavailable`, `no-match` (company names) or `skipped` (run limit reached)
- `valid`: `true` or `false` only when there is an answer; `null` when the number was not checked and has no earlier answer
- `fromEarlierRun`: `true` when the check limit or your spending limit stopped this run's check of a number with an earlier result. `status`, `valid`, `name`, `address`, `consultationNumber` and `checkedAt` are then that earlier result, not a new verification. A number that was only `skipped` before has no earlier result: it is `skipped` again, with `fromEarlierRun: false`
- `vatNumber` (normalized, with prefix), `vatNumberWithoutPrefix`, `countryCode` (VIES code, so Greece is `EL`), `countryName`
- `name`, `address`, `addressLines`: as registered, where the member state discloses them; `detailsWithheld: true` when a valid number comes without them (Germany, for example)
- `consultationNumber`, `requesterVatNumber`, `checkedAt`: your evidence that the number was valid on that date
- `statusReason`: one sentence explaining the result, for your users
- `unavailable`: `code` (VIES error), `viesStatus`, `attempts`, `waitedSeconds`, `retryAfter`
- `pendingSince`, `carriedOver`, `doubleChecked`
- `changeType` per record: `added` (first check), `unchanged` (also for a record with `fromEarlierRun: true`, which was not checked again), or `changed` with a summary such as `NL123456789B01 (ACME B.V.): no longer valid (valid on 2026-09-01)`

| Status | Meaning | Charged |
| --- | --- | --- |
| `valid` | VIES confirms the number is valid for intra-EU transactions | Yes |
| `invalid` | VIES answered: not valid (not registered, no longer active, or not enabled for cross-border trade) | Yes |
| `invalid-format` | Cannot be a VAT number of that member state (wrong length or characters); VIES is not asked | No |
| `outside-vies` | A country VIES does not cover, such as `GB`, `CH` or `NO` | No |
| `unavailable` | VIES or the member state did not answer within the maximum wait. **Not** an answer about the number | No |
| `no-match` | No company with a similar name and a valid VAT number was found. `match.alternatives` lists the similar companies without their VIES result (`not-checked`): VIES results come with a charged match | No |
| `skipped` | Not checked because the run's check limit or your spending limit was reached. A number with an earlier result other than `skipped` keeps that result instead (`unchanged`, with `fromEarlierRun: true` and a `statusReason` saying it was not checked in this run) | No |

A German number checked during an outage: not checked, not charged, and checked again in the next run of the task.

```json
{
  "envelopeVersion": 1,
  "id": "vat:DE143454214",
  "scope": "vat:DE143454214",
  "changeType": "added",
  "sourceUrl": "https://ec.europa.eu/taxation_customs/vies/",
  "scrapedAt": "2026-09-15T16:25:00.000Z",
  "firstSeenAt": "2026-09-15T16:25:00.000Z",
  "hash": "7257a435b5f7582eed8bafd9f6162e9acb0168d22dd42f19da6885189f491109",
  "previousHash": null,
  "changes": [],
  "summary": "Added: DE143454214",
  "diffText": null,
  "payload": {
    "input": "DE143454214",
    "inputType": "vat-number",
    "status": "unavailable",
    "valid": null,
    "statusReason": "Germany's VAT service did not answer VIES (MS_UNAVAILABLE) (tried 5× over 1 min). This says nothing about the number: it was not checked. It is checked again in the next run of this task.",
    "countryCode": "DE",
    "countryName": "Germany",
    "vatNumber": "DE143454214",
    "vatNumberWithoutPrefix": "143454214",
    "name": null,
    "address": null,
    "addressLines": [],
    "detailsWithheld": null,
    "consultationNumber": null,
    "requesterVatNumber": null,
    "checkedAt": null,
    "fromEarlierRun": false,
    "unavailable": {
      "code": "MS_UNAVAILABLE",
      "reason": "Germany's VAT service did not answer VIES (MS_UNAVAILABLE) (tried 5× over 1 min). This says nothing about the number: it was not checked. It is checked again in the next run of this task.",
      "viesStatus": "Available",
      "attempts": 5,
      "waitedSeconds": 75,
      "retryAfter": null
    },
    "pendingSince": "2026-09-15T16:25:00.000Z",
    "carriedOver": false,
    "doubleChecked": false,
    "match": null
  }
}
```

### Pricing

Pay only for answers.

| Event | Name | What it means | Price |
| --- | --- | --- | --- |
| `apify-actor-start` | Actor start | Charged by Apify when a run starts: once for a run of up to 1 GB of memory, and once more for every extra GB. It covers the first five seconds of compute. | $0.00005 per run up to 1 GB of memory ($0.05 per 1,000) |
| `vat-number-validated` **(primary)** | VAT number validated | VIES gave a definite answer for one VAT number: valid or invalid, with the consultation number when you provide your own VAT number. Never charged when VIES or the member state did not answer. | $0.004 per VAT number ($4.00 per 1,000) |
| `company-matched` | Company matched | A company name was matched to a VAT number that VIES confirmed as valid, including the validation itself. No charge without a confirmed match. | $0.02 per matched company ($20.00 per 1,000) |

Unavailable numbers, impossible formats, countries outside VIES, company names without a confirmed match (their records list the similar companies found, without VIES results) and test-service checks cost nothing. The time spent waiting for a member state is on us. 1,000 numbers with answers cost $4.00; a company name that is confirmed costs a match on top.

### FAQ

#### What exactly does `invalid` mean?

VIES answered that the number is not valid for intra-Community transactions. That can mean it does not exist, is no longer active, or exists nationally but is not enabled for cross-border trade (some member states register that separately). In all cases, reverse charge is not supported by that answer.

#### Why are name and address empty for a valid German number?

Germany (and some other member states) does not disclose them through VIES. `detailsWithheld` is then `true`. If you give a company name, the Actor can still compare it through the name sources above.

#### How long does a run take?

Normally a few seconds per hundred numbers. During an outage, numbers from the affected member state wait up to the maximum wait you set; numbers from other member states are not delayed.

#### Is bulk VAT number validation through VIES allowed, and is it GDPR-compliant?

VIES may be used to verify the VAT numbers of your business partners, for example to draw up your own invoices; building or reselling a database from it is not allowed. This Actor fits that purpose: every check is live, for the numbers you provide, and results go only to your own Apify account. There is no shared cache between users.

VAT numbers of sole traders are personal data under the GDPR, and VIES returns their registered name and address. This Actor therefore returns name and address only as VIES provides them, for the numbers you ask about; keeps the last result per number (for change detection and retries) only in the key-value store `vies-vat-validator-state` of your own Apify account; skips French sole traders (entrepreneurs individuels, whose company name is the owner's name) and companies that opted out of publication in the name search; and reads only names, countries and identifiers from Peppol business cards, which businesses publish themselves for e-invoicing (never contacts, addresses or free text).

Results are kept per task or state key, and runs without either share one set. A result is deleted by the first run with the same task or state key that starts more than 35 days after the last run that checked the number; a run that reached the number after its check limit or your spending limit was used up counts as checking it, a run that left it out because its memory was full does not, and a run that fails deletes nothing. A run in which nothing fits in memory fails, unless deleting results that are due for deletion makes room: then it deletes them and checks nothing. Results of a task or state key that does not run again stay until you delete them: delete the store in Apify Console (Storage, Key-value stores), or only the records whose key starts with the task ID or state key and a dot (`default.` for runs without either), while no run with that state is working.

#### What are the limitations?

- VIES publishes no rate limits; the Actor paces itself per member state and reacts to throttling, but a member state that throttles for longer than the maximum wait still yields `unavailable` numbers.
- Consultation numbers require your own valid EU VAT number; VIES rejects the whole request otherwise, and the run stops before charging.
- Company name matching depends on the coverage of the Peppol Directory outside France.
- One run checks what its memory holds (15,000 numbers and company names with the default 1 GB, 55,000 with 4 GB), fewer when its task or state key keeps many results from earlier runs, and none once a state that keeps getting new numbers is full; see **Batch size and memory** under Input.
- This Actor is not affiliated with the European Commission. It uses the public VIES REST API and, for company names, the public Peppol Directory and French company register.

### Related Actors

- [EU Company ID Resolver: turn a VAT number into KvK, KBO, SIREN and LEI](https://apify.com/lwsdjfls/eu-company-id-resolver), with the registered name, legal form and status from the national registers.
- [Peppol Participant Lookup: can this customer receive an e-invoice?](https://apify.com/lwsdjfls/peppol-participant-monitor) A valid VAT number says the customer is a VAT-registered business; Peppol reachability is a different question, answered from the same kind of list.
- [IBAN Validator & Bank Enricher for supplier and customer bank accounts](https://apify.com/lwsdjfls/iban-validator-enricher): the same kind of bulk check for IBANs, fully offline.

### Support

Missing something your accounting integration needs? Open an issue on the Actor page.

# Changelog

This Actor's version history is a separate document: https://apify.com/lwsdjfls/vies-vat-validator/changelog.md

# Actor input Schema

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

One per line, with country prefix (NL810433941B01, BE 0403.170.701, ATU12345678). Spaces, dots and dashes are fine; GR is read as EL. Numbers without a prefix use the default country below. One run checks up to 15,000 numbers and company names with the default 1 GB of memory and 55,000 with 4 GB; results kept from earlier runs of the task or state key take memory too. What does not fit is not checked or charged: the run ends as partial and the log says why. When nothing fits, the run fails, or deletes results due for deletion to make room for the next run.

## `companyNames` (type: `array`):

One per line as "Company name; country code", for example "Coolblue; NL". Candidates come from the Peppol Directory (all countries) and, for France, the French company register; each is confirmed in VIES. French sole traders are not searched by name.

## `defaultCountry` (type: `string`):

Country for VAT numbers without prefix and company names without country code.

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

Your own EU VAT number. VIES then issues a consultation number per check: proof that you verified your customer's number on that date, which tax authorities accept for reverse-charge invoices. Leave empty to validate without consultation numbers.

## `maxWaitMinutes` (type: `integer`):

How long a member state may keep failing (down, time-outs, too many requests) before its remaining numbers are reported as unavailable. Waiting is free: unavailable numbers are never charged.

## `retryUnavailableInLaterRuns` (type: `boolean`):

For saved tasks (or runs with a state key): numbers and companies VIES could not check are checked again in the following runs, even if they are no longer in the input, and delivered once VIES answers.

## `retryForDays` (type: `integer`):

Numbers that stay unavailable are retried for this many days after the first failed check.

## `minMatchScore` (type: `integer`):

How similar the registered name must be to the name you gave. Legal forms, punctuation and case are ignored; 100 means identical.

## `maxCandidatesPerCompany` (type: `integer`):

How many similar companies are validated in VIES per name (best names first).

## `maxChecksPerRun` (type: `integer`):

Caps the charged validations and matches in one run. Inputs above the cap are not checked: a number with an earlier result keeps it (marked fromEarlierRun), others are listed as skipped.

## `maxConcurrency` (type: `integer`):

Checks running at the same time. Per member state at most 3, and fewer when it reports too many requests.

## `useTestService` (type: `boolean`):

For integration tests, free of charge: VIES answers number 100 as valid, 200 as invalid, 300 SERVICE\_UNAVAILABLE, 301 MS\_UNAVAILABLE, 302 TIMEOUT, 400 VAT\_BLOCKED, 600 MS\_MAX\_CONCURRENT\_REQ (use with a default country). Company names are not matched.

## `webhookUrl` (type: `string`):

Receives a POST with the new and changed results after each run (for example a number that is no longer valid).

## `stateKey` (type: `string`):

Keeps results and retries separate for runs outside a saved task (for example per customer file). Tasks use their own state automatically; all runs without a task or state key share one. Runs with the same state must not overlap: a run that starts while another is working fails at once, without checks or charges.

## `proxyConfiguration` (type: `object`):

Not needed normally. Use a proxy only when VIES answers IP\_BLOCKED.

## Actor input object example

```json
{
  "vatNumbers": [
    "NL810433941B01",
    "BE0403170701",
    "IE9700053D",
    "DE143454214"
  ],
  "companyNames": [],
  "defaultCountry": "NL",
  "requesterVatNumber": "NL123456789B01",
  "maxWaitMinutes": 1,
  "retryUnavailableInLaterRuns": true,
  "retryForDays": 7,
  "minMatchScore": 80,
  "maxCandidatesPerCompany": 3,
  "maxChecksPerRun": 10000,
  "maxConcurrency": 6,
  "useTestService": false,
  "webhookUrl": "https://hooks.example.com/vat-checks",
  "stateKey": "customer-file-2026",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `validation` (type: `string`):

Status, registered name and address, consultation number and reason per input.

## `unavailable` (type: `string`):

Numbers VIES could not check, with the error, attempts and when to retry.

## `companyMatches` (type: `string`):

The VAT number found per company name, with similarity and alternatives.

## `runSummary` (type: `string`):

Checks, outages and the cost of the run.

# 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": [
        "NL810433941B01",
        "BE0403170701",
        "IE9700053D",
        "DE143454214"
    ],
    "maxWaitMinutes": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("lwsdjfls/vies-vat-validator").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": [
        "NL810433941B01",
        "BE0403170701",
        "IE9700053D",
        "DE143454214",
    ],
    "maxWaitMinutes": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("lwsdjfls/vies-vat-validator").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": [
    "NL810433941B01",
    "BE0403170701",
    "IE9700053D",
    "DE143454214"
  ],
  "maxWaitMinutes": 1
}' |
apify call lwsdjfls/vies-vat-validator --silent --output-dataset

```

## MCP server setup

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

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/VOrk123CAB75uQxCk/builds/oy24uojOuLxfhvmXO/openapi.json
