EU VAT number validator + French company lookup (VIES, SIREN) avatar

EU VAT number validator + French company lookup (VIES, SIREN)

Pricing

$10.00 / 1,000 company records

Go to Apify Store
EU VAT number validator + French company lookup (VIES, SIREN)

EU VAT number validator + French company lookup (VIES, SIREN)

Validates EU VAT numbers against the official VIES registry and enriches French SIREN/SIRET identifiers via the official INSEE-backed recherche-entreprises API. Pay-per-event: one company-record charge per successfully verified identifier.

Pricing

$10.00 / 1,000 company records

Rating

0.0

(0)

Developer

matheo daney

matheo daney

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Categories

Share

EU VAT (VIES) validator + French SIREN enrichment — invoice-ready company records

Validate any EU VAT number against the official VIES registry and turn a French SIREN or SIRET into a complete, invoice-ready company record — legal name, registered address, NAF/APE activity code, active/closed status, and VAT validity, all sourced live from official government registries. Built for AI agents and automated invoicing pipelines that need a single, trustworthy answer per identifier, not a scraped guess.

Use this Actor when…

  • An AI agent is about to generate or accept an invoice and needs to confirm the counterparty's VAT number is real before the document goes out.
  • You're building a KYB (Know Your Business) or supplier onboarding check and need official legal name + address + activity code for a list of SIREN/SIRET numbers, not just a validity flag.
  • You have a French SIREN or SIRET and need its EU VAT number (and whether that VAT number is currently valid), or the reverse: you have an EU VAT number and want the underlying French company record.
  • You need this as a tool an AI agent can call directly — via the Apify API, the Apify MCP server, or an n8n workflow — and get back one flat JSON object per identifier, ready to drop into a downstream decision (accept the invoice / reject it / flag for review).

This Actor does not scrape anything. Every field comes from two official, public, no-API-key government sources: the European Commission's VIES registry and France's recherche-entreprises.api.gouv.fr (INSEE/INPI-backed). See Limits & data sources below for exact citations.

Input

FieldTypeDefaultDescription
identifiersarray of strings(required)One identifier per array item. Two formats, auto-detected: an EU VAT number with country prefix (e.g. "FR12345678901", "DE123456789"), or a bare French SIREN (9 digits) / SIRET (14 digits) with no prefix.
includeFrenchEnrichmentbooleantrueControls the second, cross-referencing lookup only possible for French identifiers (VAT → company record, or SIREN/SIRET → VAT validity). See the field's in-console description for the exact behavior in each direction.
maxConcurrencyinteger2How many identifiers to verify in parallel (1–5). Kept conservative on purpose — see Limits & data sources.

Full field-level descriptions (written for both humans and LLM agents reading the input schema) are in .actor/input_schema.json.

Example input

{
"identifiers": ["FR39356000000", "552049447", "DE123456789"],
"includeFrenchEnrichment": true,
"maxConcurrency": 2
}

Output example

Real output from a local test run (2026-09-03) — a French VAT number resolved and fully enriched:

{
"input": "FR39356000000",
"type": "vat",
"countryCode": "FR",
"vatNumber": "39356000000",
"vatValid": true,
"vatValidationSource": "VIES",
"dgfipVatMatch": true,
"vatCheckedAt": "2026-09-04T00:10:15.952Z",
"legalName": "LA POSTE",
"address": "DIRECTION GENERALE DE LA POSTE 9 RUE DU COLONEL PIERRE AVIA 75015 PARIS",
"siren": "356000000",
"siret": "35600000000048",
"naf": "53.10Z",
"activeStatus": "active",
"sources": [
"https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number",
"https://recherche-entreprises.api.gouv.fr/search"
],
"charged": true,
"error": null
}

And a bare French SIREN, resolved and cross-checked for VAT validity in the other direction:

{
"input": "531680445",
"type": "siren",
"countryCode": "FR",
"vatNumber": "22531680445",
"vatValid": true,
"vatValidationSource": "VIES",
"dgfipVatMatch": true,
"vatCheckedAt": "2026-09-04T00:11:57.532Z",
"legalName": "TOTALENERGIES MARKETING FRANCE",
"address": "562 AVENUE DU PARC DE L'ILE 92000 NANTERRE",
"siren": "531680445",
"siret": "53168044500024",
"naf": "46.71Z",
"activeStatus": "active",
"sources": [
"https://recherche-entreprises.api.gouv.fr/search",
"https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number"
],
"charged": true,
"error": null
}

And a real DGFiP fallback in action (2026-09-04, same live run as the two examples above): VIES genuinely timed out on this French VAT number after its full retry budget — this Actor still delivered a charged, sourced, correctly-labeled record instead of an uncharged failure. Note vatValid: null — the fallback never claims a VIES confirmation it doesn't have:

{
"input": "FR35552049447",
"type": "vat",
"countryCode": "FR",
"vatNumber": "35552049447",
"vatValid": null,
"vatValidationSource": "DGFIP_FALLBACK",
"dgfipVatMatch": true,
"vatCheckedAt": null,
"legalName": "SOCIETE NATIONALE SNCF (SNCF)",
"address": "2 PLACE AUX ETOILES 93210 SAINT-DENIS",
"siren": "552049447",
"siret": "55204944776279",
"naf": "49.10Z",
"activeStatus": "active",
"sources": [
"https://recherche-entreprises.api.gouv.fr/search"
],
"charged": true,
"error": null
}

Every dataset item has exactly this set of keys, whether verification succeeded or failed — a null value means "not applicable to this identifier / this lookup wasn't attempted", never "unknown due to an error" (that's what the error field is for).

FieldMeaning
inputThe raw identifier as given.
type"vat", "siren", "siret", or "malformed".
countryCode2-letter VIES country code ("FR" for SIREN/SIRET-derived records).
vatNumberThe VAT number checked (without country prefix), if any.
vatValidtrue/false from an actual VIES response, or null if VIES never answered — never true from any other source, see Limits & data sources.
vatValidationSource"VIES" (an actual VIES response backs vatValid), "DGFIP_FALLBACK" (VIES never answered; dgfipVatMatch is the positive signal instead), or null.
dgfipVatMatchtrue/false if the DGFiP-sourced list for this SIREN was checked, null if it wasn't (non-French, or the lookup itself failed). Independent of whether VIES also answered.
vatCheckedAtISO 8601 timestamp of the VIES response, or null (including when vatValidationSource is "DGFIP_FALLBACK" — VIES was never actually reached).
legalNameOfficial registered company name.
addressOfficial registered address.
sirenFrench SIREN (9 digits), if resolved.
siretFrench SIRET (14 digits) of the relevant establishment, if resolved.
nafFrench NAF/APE activity code, if resolved.
activeStatus"active", "closed", "not_found", or null.
sourcesArray of the official API URL(s) actually consulted for this record.
chargedtrue if this record triggered a company-record charge.
errorExplanation if verification failed (implies charged: false), otherwise null.

You can download the dataset in JSON, CSV, Excel, or several other formats from the Apify Console or the API. The Console's Output tab shows a pre-labeled table view (.actor/dataset_schema.json) with the most decision-relevant columns (charged, vatValid, activeStatus, legalName) up front.

Pricing

This Actor uses Apify's pay-per-event (PPE) pricing model — see Apify's PPE documentation for how the mechanism itself works. There is a single billable event:

  • company-record — charged once per identifier for which verification genuinely completed: VIES (or, for a French SIREN/SIRET, recherche-entreprises.api.gouv.fr) returned a definitive answer, whether that answer is "valid" / "found" or a confirmed "invalid" / "not found". A confirmed negative is still a usable decision for an invoicing pipeline, so it is charged the same as a confirmed positive.

A French VAT number recovered through the DGFiP fallback (2026-09-04) is also charged, on the same event, at the same price — deliberately, not by default/oversight. Rationale: it delivers real, sourced, useful company data (legal name, address, NAF, status, and confirmation the VAT number is on DGFiP's own active list) exactly like any other successful record, and this Actor already charges the same for a confirmed-invalid VIES answer on the reasoning that a definitive negative is itself billable value — a DGFiP-confirmed positive meets that same bar. The precondition is that the output is never ambiguous about which source backs the answer (vatValidationSource, see Output above and Limits & data sources below) — a client who specifically needs VIES-grade proof can filter on vatValidationSource === "VIES" and treat anything else as unconfirmed, at no extra integration cost. This is revisited if real usage shows this reads as a surprise rather than a documented trade-off (see input schema and the section below, both written before a client would ever hit this case).

You are never charged for:

  • a malformed identifier (rejected before any API call is made);
  • a run where the upstream registry could not be reached even after automatic retries with backoff, and (for a French VAT number) the DGFiP fallback could not confirm the number either.

Target price: $0.008 per company-record ($8 per 1,000 records) — configured on the Actor's Pricing page in Apify Console, not in this code. In the code, the event is declared with Actor.charge() / the Actor.pushData(item, 'company-record') shortcut (see src/main.js and src/record.js).

Limits & data sources

VIES (VAT validation) — European Commission, POST https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number. Official Swagger/OpenAPI spec: https://ec.europa.eu/assets/taxud/vow-information/swagger_publicVAT.yaml (fetched and cross-checked 2026-09-03/04). No API key. A sandbox exists at check-vat-test-service (country DE/etc. + VAT number "100" → always valid, "200" → always invalid) — verified live 2026-09-03; this Actor deliberately never calls it, since a production invoicing tool has no reason to return sandbox data.

VIES has no published fixed rate limit, but throttles under load both per member state (MS_MAX_CONCURRENT_REQ) and globally (GLOBAL_MAX_CONCURRENT_REQ). Per the European Commission's own VIES "Technical information" page (https://ec.europa.eu/taxation_customs/vies/#/technical-information, cross-checked 2026-09-04 — see src/vies.js header for exactly how, since that page cannot be fetched/rendered by this tooling directly), those two codes plus MS_UNAVAILABLE, SERVICE_UNAVAILABLE and TIMEOUT are all transient/load-related and worth retrying; INVALID_INPUT, INVALID_REQUESTER_INFO, VAT_BLOCKED and IP_BLOCKED are definitive and never retried.

⚠️ On the Apify platform, this matters more than it would from a personal machine: outbound IPs are shared across many concurrent customers' Actor runs, so VIES's per-member-state quota can be consumed by traffic that has nothing to do with this Actor's own request rate — confirmed by a real production failure (MS_MAX_CONCURRENT_REQ on a French VAT number that passes reliably from a personal IP, build 0.1.1, 2026-09-03). Two mitigations, both in src/vies.js:

  • Patient retry with jitter: up to 6 retries (7 attempts) per identifier, exponential backoff (3s, 6s, 12s, 24s, 30s, capped at 30s) multiplied by a wide random jitter factor (0.5×–1.6×) — the jitter matters specifically because the IP is shared: without it, many Apify containers hitting the same transient fault at the same instant would retry in lockstep and re-collide. A hard 90-second total time budget per identifier bounds how long one contested identifier can hold up a batch, regardless of attempts remaining.
  • Per-country serialization: VIES's concurrency limit is per member state, so this Actor never has two of its own requests for the same country in flight at once (that would only add to its own contribution to that country's queue, for no benefit) — while requests for different countries, and all recherche-entreprises calls, still run fully in parallel.

A transient failure (retries exhausted) is never confused with a genuinely invalid identifier, nor with a DGFiP-fallback record — three different vatValid/charged combinations, never overlapping:

  • Confirmed by VIES, either way: vatValid: true or false, vatValidationSource: "VIES", charged: true, error: null — a real, usable, billable answer either way.
  • Recovered via the DGFiP fallback (French VAT numbers only, see below): vatValid: null (never true/false from this source), vatValidationSource: "DGFIP_FALLBACK", dgfipVatMatch: true, charged: true, error: null.
  • Genuine transient failure (neither source could confirm anything): vatValid: null, vatValidationSource: null, charged: false, error reads exactly "VIES temporarily unavailable after N attempt(s) (last fault: <code>): retry later." — a client can branch on all of this without reading logs (retry later / treat as a confirmed rejection / treat as DGFiP-sourced but not VIES-grade). A malformed identifier (never sent to VIES at all) gets its own, differently-worded error instead.

End-of-run retry pass (2026-09-04): VIES contention is usually momentary. Any identifier that is still a transient failure after all of the above is held back rather than pushed as final — after the whole batch finishes, this Actor waits 60 seconds (contention often clears once other identifiers have stopped competing for the same member state) and retries exactly that subset once more, together. Whatever that second pass produces (success, a confirmed-invalid answer, or still a transient failure) is what actually gets pushed and, if applicable, charged — never both a failed and a successful record for the same identifier.

Total run time budget (documented, not enforced by an extra hard timeout beyond the 90s-per-identifier one above): the worst case is bounded by how many identifiers share the most-contested country code, since same-country VIES calls always serialize. For K identifiers sharing that country: phase 1 ≈ K × 90s, + a fixed 60s wait, + phase 2 ≈ K × 90s again if still contested. For example, 2 same-country VAT numbers both genuinely contested (observed for real in this Actor's own testing) → up to ≈ 420s (7 minutes) for that pair, run alongside the rest of the batch rather than blocking it.

Apify Proxy was investigated (2026-09-04) and deliberately NOT used. The hypothesis was that Apify's shared outbound IPs cause MS_MAX_CONCURRENT_REQ by mixing this Actor's traffic with other customers' runs at the IP level. The best evidence found — cross-checked across a specialist third-party VIES provider and independent developer reports, no official EC statement found either way (see the full citation trail in the project's session history) — is that this VIES limit is a global counter per member state, not scoped to the caller's IP address: it reflects total worldwide demand on that member state's backend, not "how many Apify users happen to share an IP with us" at that moment. If that is correct, routing through Apify Proxy (residential or datacenter) would not fix this specific failure mode — datacenter proxy groups are, per Apify's own documentation, also a pool "shared across multiple customers" unless paid dedicated groups are purchased (custom, unpublished pricing), so it would not even change the sharing situation in the way the hypothesis assumed. The end-of-run retry pass above is the mitigation actually implemented instead.

recherche-entreprises.api.gouv.fr (French company data) — maintained by DINUM (Direction interministérielle du numérique), backed by INSEE/SIRENE and INPI. GET https://recherche-entreprises.api.gouv.fr/search?q=<9 or 14 digits>. No API key. Documented hard limit: 7 requests/second per IP, 30/second per ASN (source: the API's own OpenAPI description, fetched 2026-09-03 — see https://recherche-entreprises.api.gouv.fr/openapi.json); exceeding it returns HTTP 429 with a Retry-After header, which this Actor honors. Licence: Licence Ouverte 2.0 / Open Licence 2.0 (Etalab).

maxConcurrency defaults to 2 (lowered from 3 after the 2026-09-04 resilience fix above) and is capped at 5, and is never allowed to exceed the number of identifiers in the batch. It bounds SIRENE parallelism directly, and VIES parallelism across different countries — same-country VIES calls are always serialized regardless of this setting (see above).

French DGFiP fallback (2026-09-04) — and why it is NOT the same thing as a VIES check

For a French VAT number specifically, if VIES is still a transient failure after everything above (retries, per-country serialization, the end-of-run retry pass), this Actor makes one more attempt: it checks whether that number appears in recherche-entreprises's tva field for the corresponding SIREN — a direct pass-through of a dataset DGFiP itself publishes daily.

What that field actually is (traced to source, 2026-09-04): the API's own OpenAPI description labels it "Liste des numéros de TVA intracommunautaire français actifs (source : DGFiP)". Following the data pipeline that populates it — annuaire-entreprises-data-gouv-fr/search-infra, workflows/data_pipelines/tva/ (github.com/annuaire-entreprises-data-gouv-fr/search-infra, read 2026-09-04) — it is a daily, unmodified pass-through of a CSV published at https://data.economie.gouv.fr/explore/assets/numeros-de-tva-intracommunautaires-francais/. That dataset's own catalog metadata (data.economie.gouv.fr OpenDataSoft API, /api/v2/catalog/datasets/numeros-de-tva-intracommunautaires-francais, fetched 2026-09-04) states: Publisher: DGFIP, description "Population des numéros de TVA intracommunautaire français actifs", refreshed daily. This is real, sourced, government-published data — not a check-digit computed on the fly for an arbitrary SIREN (the check-digit formula is documented separately in src/identifier.js, used only to derive which number to look up, never to fabricate a "valid" answer).

What it is not, and why VIES still matters: EU law makes VIES specifically — not a national tax authority's own records — the reference for the buyer's VAT ID when a seller applies the intra-Community VAT exemption. Directive 2006/112/EC ("the VAT Directive"), Article 138, as amended by Council Directive (EU) 2018/1910 (in force since 2020-01-01, the EU's "2020 Quick Fixes"), made the buyer's VAT identification number being valid specifically in VIES a substantive condition for that exemption — not a mere formality a seller can cure after the fact. A DGFiP daily extract, however faithfully sourced, is a different instrument: it does not carry that same legal weight for zero-rating an intra-EU supply, and it reflects DGFiP's own daily-refreshed view, not VIES's real-time cross-border-harmonized one (a lag of up to ~24h is possible between the two).

Consequently: vatValid is never set to anything but null by this fallback — only an actual VIES response ever sets it. The fallback's positive signal lives entirely in dgfipVatMatch: true + vatValidationSource: "DGFIP_FALLBACK", so a client cannot mistake this for a VIES-confirmed record by reading vatValid alone, and can trivially filter it out (vatValidationSource === "VIES") if a VIES-grade proof is what they actually need. The fallback only ever produces a positive result — a SIREN found but with no matching entry in the DGFiP list is left as an ordinary, uncharged transient failure (that absence is too weak a signal, on its own, to report as a confirmed rejection).

Update frequency: both registries are updated by their respective national administrations on their own schedule (typically daily for SIRENE); vatCheckedAt always reflects the exact moment of the check, not a cached value — this Actor never caches results between runs.

This Actor returns company data only — legal name, registered address, activity code, administrative status, VAT number. It never returns personal data about natural persons (no director/manager/beneficial-owner names, no personal contact details), even though the underlying recherche-entreprises.api.gouv.fr API can expose executive/officer names (dirigeants) via other query parameters. This Actor does not request, read, or forward that field.

Both source APIs are public, unauthenticated, official government registries designed for exactly this kind of automated lookup; no data is scraped, and no personal data is stored by this Actor beyond the lifetime of the run's dataset (which you control and can delete like any other Apify dataset).

Integrations (API, MCP, n8n)

  • Apify API — run this Actor from any HTTP client via POST https://api.apify.com/v2/acts/<your-username>~eu-vat-siren-check/run-sync-get-dataset-items (or the async run + get-dataset-items pair) with your API token. See the Actor's API tab in Apify Console for ready-to-copy snippets (cURL, Python, JS).
  • MCP (Model Context Protocol) — expose this Actor as a tool to any MCP-compatible AI agent via the Apify MCP server (https://mcp.apify.com, or @apify/actors-mcp-server for local/self-hosted use). Once discovered, an agent can call it directly with a natural-language list of identifiers.
  • n8n — use the official Apify node for n8n (@apify/n8n-nodes-apify) to run this Actor as a step in a workflow — e.g. triggered right before an "Invoice" node, feeding it vatValid/vatValidationSource/activeStatus to gate the send (check vatValidationSource === "VIES" specifically if the workflow needs VIES-grade proof, not just any sourced positive answer).