If you onboard B2B customers, suppliers or marketplace sellers in Europe, sooner or later someone asks: is this company real, active, and is its VAT number valid?
Answering that by hand means opening several tabs. You check the VAT number in the European Commission's VIES service, look the company up in the national register (Sirene/RNE in France, Brønnøysund in Norway, PRH in Finland, ARES in Czechia), search GLEIF for an LEI, and for French companies look through BODACC for insolvency notices. That's fine for one company. For a CSV of 2,000 it isn't.
This tutorial shows how to do the whole check in bulk with the EU Company Verification (KYB): VIES VAT, LEI, SIREN Actor on Apify, which our team at Ostrata built and maintains. You send a list of strings and get back one record per company with a verdict and the machine-readable reasons behind it. At the end there's a second Actor for when you only have a website, not a company number.
What you can put in
The input is a list. Each line can be:
- a company name (
Doctolib), searched in the default country's register - a French SIREN or SIRET (
552 120 222), a Norwegian org number, a Finnish Y-tunnus or a Czech IČO - an EU VAT number (
FR40303265045,DE811569869,IE6388047V) - an LEI (
969500BPSLCSC039UV67) - any of the above with a country prefix to force the country:
NO: Equinor,CZ: Alza
Identifiers are detected automatically. The other input fields are optional:
| Field | Default | What it does |
|---|---|---|
defaultCountry |
FR |
Register used for bare names: FR, NO, FI, CZ, or ANY (GLEIF LEI index) |
checkVat |
true |
Live VIES check (the VAT number is derived from the SIREN for France) |
checkLei |
true |
GLEIF LEI lookup and status |
checkInsolvency |
true |
BODACC collective proceedings (last 3 years) for France, registry flags elsewhere |
maxCandidatesPerQuery |
1 |
How many matches to return per name search |
minNameScore |
"0.5" |
Drop weaker name matches; below 0.85 the verdict becomes review
|
excludeSoleProprietors |
true |
Name searches return legal entities only |
includeRaw |
false |
Attach the raw registry payload |
concurrency |
3 |
Parallel queries (each upstream API is also rate-limited per host) |
Step 1: Run it from the Console
- Open apify.com/ostrata/eu-company-check and click Try for free. A free Apify account works.
- Paste your list into Companies to check, one per line.
- Leave the defaults on, or switch off the LEI check if you don't need it.
- Click Start. Results appear in the Output tab and can be exported as JSON, CSV or Excel.
Step 2: Call it with curl
The run-sync-get-dataset-items endpoint runs the Actor and returns the dataset in one HTTP call. maxTotalChargeUsd puts a hard cap on what the run may charge:
curl -s -X POST \
"https://api.apify.com/v2/acts/ostrata~eu-company-check/run-sync-get-dataset-items?maxTotalChargeUsd=1" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"companies": ["FR40303265045", "Blablacar", "440471100", "NO: Equinor", "IE6388047V"],
"defaultCountry": "FR",
"checkVat": true,
"checkLei": true,
"checkInsolvency": true
}'
Synchronous runs time out after 300 seconds. For thousands of rows, start an asynchronous run instead (POST /v2/acts/ostrata~eu-company-check/runs) and read the dataset when it finishes, or use a client library as below.
Step 3: JavaScript (apify-client)
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ostrata/eu-company-check').call(
{
companies: ['FR40303265045', 'Doctolib', 'NO: 923609016', 'CZ27082440'],
defaultCountry: 'FR',
checkLei: false, // faster if you don't need LEIs
},
{ maxTotalChargeUsd: 2 }, // stop before spending more than $2
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const c of items) {
console.log(c.query, c.verdict, c.legalName, c.vatValid, c.reasons.join(','));
}
Step 4: Python (apify-client 3.x)
import os
from decimal import Decimal
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ostrata/eu-company-check").call(
run_input={
"companies": ["DE811569869", "IT00743110157", "ES: Inditex"],
"defaultCountry": "ANY",
},
max_total_charge_usd=Decimal("1"),
)
needs_review = [
row for row in client.dataset(run.default_dataset_id).iterate_items()
if row["verdict"] != "verified"
]
print(needs_review)
Filtering on verdict != "verified" is the useful pattern: your team only looks at the rows that need a human.
What comes back
Here is a trimmed record from one of our own test runs (19 Oct 2026), for the VAT number FR40303265045:
{
"query": "FR40303265045",
"queryType": "vat",
"verdict": "verified",
"reasons": ["registry_status_active", "vat_valid_in_vies", "lei_issued"],
"warnings": [],
"legalName": "SODIMAS",
"country": "FR",
"registrationNumber": "303265045",
"registrationNumberType": "SIREN",
"registryStatus": "active",
"vatNumber": "FR40303265045",
"vatValid": true,
"lei": "969500GZTMR1IRVURM23",
"insolvencyFlag": false,
"company": {
"legalForm": "SA à conseil d’administration",
"registrationDate": "1975-08-21",
"address": { "postalCode": "26600", "city": "PONT-DE-L'ISERE", "country": "FR" },
"activity": { "code": "28.22Z", "scheme": "NAF rev.2" },
"employees": "250-499"
},
"vat": { "valid": true, "name": "SA SODIMAS", "source": "VIES VAT number validation (European Commission)" }
}
Every record also carries a sources array naming each register used and its licence.
The same test runs gave these verdicts across a mixed list:
| Input | Verdict | Legal name | Why |
|---|---|---|---|
552 120 222 |
verified | SOCIETE GENERALE | active in Sirene, VAT valid in VIES, LEI issued |
Blablacar |
verified | COMUTO | name match 1.0 (trade name), LEI issued |
440471100 |
inactive | LB | ceased in the register, VAT not valid, liquidation notice in BODACC |
NO: Equinor |
verified | EQUINOR ASA | active in Brønnøysund (Norway is not in VIES, so no VAT check) |
CZ27082440 |
verified | Alza.cz a.s. | active in ARES, VAT valid in VIES |
IE6388047V |
verified | GOOGLE IRELAND LIMITED | VAT valid in VIES (no Irish register adapter), LEI issued |
ES: Inditex |
review | INDUSTRIA DE DISEÑO TEXTIL, S.A. | GLEIF entity status not confirmed |
zzqx nonexistent company 4821 |
not_found | – | not charged |
The Blablacar row is a good example of why the record has warnings. VIES answered MS_MAX_CONCURRENT_REQ (the French VIES node was busy), so the record came back with vatValid: null and the warning vat_check_unavailable:MS_MAX_CONCURRENT_REQ. A busy VIES node is never reported as an invalid VAT number.
You only have a website? Use Website to Company
Lead lists and signup forms often give you a domain, not a company number. The companion Actor Website to Company: Impressum & Mentions Légales Scraper reads a site's legal notice (mentions légales, Impressum, imprint), extracts the SIREN, VAT number, trade-register number or LEI, and then runs the same verification.
curl -s -X POST \
"https://api.apify.com/v2/acts/ostrata~website-company-check/run-sync-get-dataset-items?maxTotalChargeUsd=1" \
-H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
-d '{"websites": ["qonto.com", "zalando.de", "alan.com"], "verify": true}'
In our tests, qonto.com came back as QONTO, SIREN 819489626, VAT FR10819489626, valid in VIES. zalando.de returned VAT DE260543043 (valid) and the trade-register entry HRB 158855 B at Amtsgericht Charlottenburg Berlin, but legalName: null, because German VIES never returns names. Several sites (doctolib.fr, malt.fr, blablacar.fr, personio.de) were reported as blocked by bot protection and were not charged: the Actor makes plain HTTP requests, respects robots.txt and does not try to get around protection.
Other input fields: verify (off = identification only), checkVat, checkInsolvency, checkLei (off by default because GLEIF allows 60 requests/minute), excludeSoleProprietors, maxPagesPerSite (default 4) and concurrency.
Use it from an AI agent (MCP)
Both Actors work as tools in any MCP client (Claude, Cursor, VS Code and others) through Apify's hosted MCP server:
{
"mcpServers": {
"eu-company-check": {
"url": "https://mcp.apify.com?tools=ostrata/eu-company-check,ostrata/website-company-check"
}
}
}
On first connection your client opens an Apify sign-in (OAuth). You can also pass Authorization: Bearer <APIFY_TOKEN> as a header. Then ask something like: "Check whether FR40303265045 and NO: Equinor are active companies with valid VAT numbers." The run is billed to the Apify account that's connected.
Pricing
Both Actors are pay-per-event, prices as listed on the Apify Store on 19 Oct 2026:
-
EU Company Verification (KYB): $0.005 per company found (verdict
verified,revieworinactive), so $5 per 1,000.not_found,invalid_inputanderrorrows are free. - Website to Company: $0.004 per website where an identifier is found, plus $0.005 when that company is verified. Blocked sites, sites with no identifier and errors are free.
- Both have the standard Apify start event of $0.00005 per run.
Run as one list, the eight inputs in the table above would cost 7 × $0.005 = $0.035, because the not-found row is free.
Honest limits
- Full register adapters exist for France, Norway, Finland and Czechia. Elsewhere, VAT numbers are checked in VIES and names are searched in GLEIF, which only covers companies that hold an LEI.
- Germany, Spain and some other member states never return a name or address through VIES, so you get
vatValidbutlegalName: null. - VIES nodes go down or throttle. The Actor retries, then returns
vatValid: nullwith a warning instead of a false "invalid". - Website to Company does not render JavaScript, so pages built only client-side are not read.
- Neither Actor returns people's data: no directors, shareholders, emails or phone numbers. Sole proprietors are excluded by default.
- Results reflect the sources at query time. It is a data tool, not legal advice.
If your use case needs another country register, tell us in the Actor's Issues tab on Apify. UK, Germany and the Netherlands are the next ones we're looking at.
Written by the Ostrata team (ostrata.io).
Top comments (0)