# VAT Validator API - VIES Check, Company Name & Address (`stas_k/vat-validator`) Actor

When VIES goes down it answers nothing, and a checker that calls that INVALID is guessing. This one separates a wrong number, an unregistered one and VIES being unreachable. Offline checksum first, unanswered numbers are free.

- **URL**: https://apify.com/stas_k/vat-validator.md
- **Developed by:** [Stas K](https://apify.com/stas_k) (community)
- **Categories:** Business, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 vat number checkeds

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

## VAT Validator API - VIES Check, Company Name and Address

**The problem this solves.** Every VAT checker has the same hidden failure: when
VIES (the EU registry that actually knows whether a number is registered) goes
down, or answers only part of the time, the checker reports `invalid`. The buyer
then flags a real customer as a tax fraud, blocks a legitimate invoice, or
rejects a supplier who is perfectly fine.

VIES is not a stable API. It goes down, it throttles, it answers
`actionSucceed: false`, and sometimes it answers with `valid: null` plus an
error code. That silence is not an answer, and this actor refuses to pretend
otherwise: a null is retried, and if VIES still has not spoken, the row says so.

### Three answers, never two

| Status | What it means | Charged |
|---|---|---|
| `VALID` | VIES confirmed the number is registered, with the company name and address | yes |
| `INVALID_VIES` | VIES answered and said the number is not registered | yes |
| `INVALID_CHECKSUM` | Arithmetic proves the number wrong, with the digits it should have | yes |
| `NOT_IN_VIES` | Country outside VIES (GB, NO, CH): the national checksum passes, registration unknown | yes |
| `INVALID_FORMAT` | Wrong length or characters for that country | no |
| `UNVERIFIED` | **VIES did not answer.** Not proof of anything, retry later | no |
| `NOT_CHECKED` | VIES switched off in the input: `valid` is `null`, only `checksumValid` is filled | no |
| `UNSUPPORTED_COUNTRY` | Prefix is not a VAT country we know | no |
| `NOT_ATTEMPTED` | The run stopped or ran out of time before this number | no |
| `ERROR` | Our own failure on this row, with the reason in `reason` | no |

A run where VIES was down returns rows, not a false verdict. You never pay for a
question nobody answered.

### Why the offline layer matters

Every number first goes through the national shape and checksum. A German number
with a bad check digit is rejected in microseconds, with the digit it should have
had, without spending a VIES call. That means bigger batches fit inside the VIES
rate limits, and a mistyped number is caught before it becomes a "not registered"
false alarm.

Checksums are implemented for AT, BE, CH, CZ, DE, DK, EE, ES, FI, FR, GB, HR, HU,
IE, IT, LU, NL, NO, PL, PT, SE, SI and XI. Where VIES covers the country, the
reference numbers are ones VIES reports as valid. GB, NO and CH are not in VIES,
so there the reference is the published national rule cross-checked against two
independent implementations: for GB both modular methods (a remainder of zero is
written as check digits 00, and 97 also appears in older records), the
twelve-digit branch form, and the GD and HA ranges for government departments and
health authorities; for Switzerland the MWST, TVA and IVA labels are dropped
before the sum, and for Norway the MVA label likewise.

Two rules keep this layer honest. Where the algorithm covers every number a
country issues, a failed check is a verdict, and the row names the digit it
expected. Where it does not, the row goes to VIES: the Dutch eleven-proof, for
example, is not satisfied by every number the Dutch registry issues, so a failure
there is reported in `checksumValid` and VIES decides, rather than a real firm
being rejected. For countries without an implemented checksum the field
`checksumImplemented` is `false` and VIES alone decides. A false `invalid` on a
real customer number is the one mistake this actor must never make.

### What you get per row

`rowIndex`, `input`, `country`, `vatNumber`, `status`, `valid`, `reason`,
`companyName`, `companyAddress` (what VIES returns), `checksumImplemented`,
`expected` (which digits were expected), `checksumValid`, `fromCache`,
`viesAnswered`, `charged`, `checkedAt`.

Rows arrive as they finish, so with `maxConcurrency` above 1 they are not in
input order. Sort by `rowIndex` when order matters.

### Price

Pay per event `vat-checked`: once per number whose verdict was actually produced.

Free: malformed numbers, unknown countries, numbers VIES could not answer,
answers taken from cache, and numbers pulled out of a pasted document without a
country prefix written as one. A prefix counts only when it is uppercase and
glued to the digits (`DE811569869`, `CHE432825998`). Everything else from free
text is checked and not charged: a document number looks exactly like a VAT
number, and "Invoice No 259137088" reads "No" as Norway if you let it. Numbers
written with spaces and a lowercase prefix ("de 811 569 869") are read as one
number and are still free.

### Capacity

One VIES question takes roughly 2 to 6 seconds, and the input cap is 4,000
numbers per run, which is also the realistic ceiling for one hour: 4,000
questions at `maxConcurrency: 8` take most of the hour, and at `3` they do not
fit at all. If the time budget runs out, the remaining rows come back as
`NOT_ATTEMPTED` with the reason, and nothing is charged for them. Split longer
lists into runs of about 2,000 at concurrency 3.

### Duplicates

The same number written twice, with or without spaces and prefix, is checked once
and billed once. `inputRows` in `SUMMARY` counts every row you sent, all spellings
included; `uniqueNumbers` counts the distinct numbers, and `vatNumbers` counts the
rows actually returned.

### If the dataset refuses a row

We retry the write twice. If it still fails, the row is written to the key-value
record `DELIVERY_FAILURES` with its number and whether it was charged, and the run
stops with `stopReason: delivery-error` - the record is what you need to claim a
refund, and stopping keeps us from piling charges into a broken dataset.

### Example

Input:

```
DE811569869
FR40303265045
DE811569860
XX123
```

Output (trimmed):

```
{"input": "DE811569869", "status": "VALID", "valid": true, "reason": "VIES confirmed the number"}
{"input": "FR40303265045", "status": "VALID", "valid": true, "companyName": "SA SODIMAS",
 "companyAddress": "11 RUE AMPERE 26600 PONT DE L ISERE"}
{"input": "DE811569860", "status": "INVALID_CHECKSUM", "valid": false, "expected": "last digit should be 9"}
{"input": "XX123", "status": "UNSUPPORTED_COUNTRY", "valid": null, "reason": "XX is not a known VAT country"}
```

### What this actor does not claim

It does not tell you whether a company is trustworthy, solvent, or has paid its
taxes. It tells you what VIES says today, and whether VIES actually said
anything. For GB, NO and CH, VIES has no record: those rows carry the national
checksum verdict and say so.

Answers are cached for `cacheMinutes` in the `vat-check-cache` key-value store of
the account that runs the actor, so a cached answer may come from an earlier run
of yours: it is the same public record of the same number. A cached answer is not
charged.

Ideas, bugs or a country you need checksum coverage for: open an issue on the
actor page.

#### Spending cap

If you do not set a spending cap, Apify applies its own default (about $4.63 per run). When that cap is reached the run stops early: the remaining items are skipped and you are charged only for what was actually delivered. For a full pass, set `maxTotalChargeUsd` in the run options.

# Actor input Schema

## `vatNumbers` (type: `string`):

One per line, with the country prefix: DE259137088, FR 40 303265045, NL004495445B01. Spaces, dots and dashes are ignored. Up to 4,000 per run.

## `text` (type: `string`):

Used only when 'VAT numbers to check' is empty: numbers are pulled out of an invoice, an email or any pasted text, in the order they appear. A bare number without a country prefix glued to it is checked but never charged.

## `countryHint` (type: `string`):

Two-letter code, used only for numbers that come without a prefix, for example a column of German numbers pasted without DE.

## `checkVies` (type: `boolean`):

On by default. Turn it off for a pure offline run: shape and checksum only, no VIES calls, and the status says so.

## `cacheMinutes` (type: `integer`):

0 turns the cache off. A cached answer is not charged: the number is the same public record, whoever asked first.

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

VIES limits simultaneous requests; 3 is safe, 8 is the ceiling here. One question takes 2 to 6 seconds, so 3 handles roughly 2,000 numbers per hour and 8 roughly 4,000 in most of an hour. Lower it if you see many UNVERIFIED rows.

## `timeBudgetSeconds` (type: `integer`):

The run stops starting new VIES calls after this many seconds and reports the rest as NOT_ATTEMPTED, uncharged. Keep it below timeoutSecs of the run.

## `viesTimeoutSeconds` (type: `integer`):

VIES is often slow. A timeout does not become 'invalid': the row is UNVERIFIED and not charged.

## Actor input object example

```json
{
  "vatNumbers": "DE811569869\nFR40303265045\nIT00743110157\nDE811569860\nXX123",
  "checkVies": true,
  "cacheMinutes": 1440,
  "maxConcurrency": 3,
  "timeBudgetSeconds": 3300,
  "viesTimeoutSeconds": 30
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per VAT number

# 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": `DE811569869
FR40303265045
IT00743110157
DE811569860
XX123`
};

// Run the Actor and wait for it to finish
const run = await client.actor("stas_k/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": """DE811569869
FR40303265045
IT00743110157
DE811569860
XX123""" }

# Run the Actor and wait for it to finish
run = client.actor("stas_k/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": "DE811569869\\nFR40303265045\\nIT00743110157\\nDE811569860\\nXX123"
}' |
apify call stas_k/vat-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stas_k/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/fKtLkgGNziei2zdIt/builds/z45lShVu375ysmoTI/openapi.json
