EU VAT Number Validator (VIES) avatar

EU VAT Number Validator (VIES)

Pricing

from $2.00 / 1,000 vat validateds

Go to Apify Store
EU VAT Number Validator (VIES)

EU VAT Number Validator (VIES)

Compliance Operations System for EU VAT — detect deregistered or mismatched counterparties, escalate by SLA, auto-generate ticket-ready actions. Bulk-validate against the official VIES API across all 27 EU countries + XI. Audit-grade consultation references, change detection, risk scoring.

Pricing

from $2.00 / 1,000 vat validateds

Rating

0.0

(0)

Developer

ryan clinton

ryan clinton

Maintained by Community

Actor stats

0

Bookmarked

11

Total users

2

Monthly active users

2 days ago

Last modified

Share

EU VAT Number Validator (VIES) — Compliance Operations System

Detect, decide, escalate, and track EU VAT compliance issues — from first signal to SLA-breach escalation. Every alerted row ships with a stable recommendedAction (block_invoice, hold_payment, request_certificate, review, retry_later), a 2–4 step actionPlaybook, a board-level businessImpact line, an optional financialRiskEstimate (when you supply expected invoice amounts), daysOpen + slaBreached + escalationLevel (0–3), and a portfolio-level summary.repeatOffenders + riskTrend. Audit-grade EU consultation references for tax filings, change detection between scheduled runs, entity verification with match scoring — $0.002 per VAT validated.

This is not just a VAT validator — it is a VAT compliance monitoring and automation system. Designed specifically for ongoing VAT compliance monitoring across scheduled runs.

Pay-per-use EU VAT validation API — no API key, no subscription, $0.002 per validation. JSON output, webhook-ready, no parsing required. Apify's free tier covers ~2,500 validations per month.

Also works as a full VAT compliance monitoring + automation system (all advanced features opt-in).

EU VAT Number Validator is a stateful EU VAT compliance engine that wraps the official VIES REST API operated by the European Commission and adds the layers VIES alone does not give you: audit-grade EU consultation reference numbers (when you supply your own VAT), cross-run change detection (catch deregistrations and renames between scheduled runs), entity verification (match the VIES name against your expected name), per-VAT data completeness scoring, country reliability hints, parsed trader fields, and stable failure classification with actionable next-step recommendations. 20 concurrent workers with per-country rate limiting + per-country circuit breaker. No API key, no subscription. Cache hits not charged.

What is a VAT Compliance Operations System?

A VAT Compliance Operations System combines four layers most VAT validators ship separately or not at all:

  • Detection — bulk-validate VAT numbers against the official EU VIES database, including country format pre-checks and cross-run change detection (deregistration, name change, address change)
  • Decision — convert raw signals into stable, machine-routable actions (block_invoice, hold_payment, request_certificate, review, retry_later, monitor_only) with priorities and 2–4 step playbooks
  • Escalation — track each open issue's age, SLA breach status, and escalation level (0–3) across scheduled runs
  • Audit — preserve EU consultation reference numbers (recognised by tax authorities under VAT Directive Art 138) and ship a drop-in compliance archive per run

This actor does all four. Most validators stop at Detection. This is not just a VAT validator — it is a VAT compliance monitoring and automation system.

Use this actor when you need to

  • Automatically detect invalid or deregistered EU VAT numbers across your customer or supplier database
  • Block zero-rated invoices to counterparties whose VAT became invalid since the last run
  • Verify a supplier's claimed legal name against the VIES-registered name before issuing payment
  • Schedule weekly or monthly EU VAT compliance monitoring with deregistration alerts
  • Generate audit-grade EU consultation reference numbers for tax filings under VAT Directive Article 138
  • Wire VAT-compliance signals into Slack, Microsoft Teams, Jira, Linear, GitHub Issues, Salesforce, HubSpot, PagerDuty, Zapier, Make, or n8n via webhooks
  • Replace manual VIES portal lookups for compliance teams handling 100+ counterparties
  • Track repeat offenders, SLA breaches, and risk trend across scheduled runs (compliance ops dashboard)

EU VAT Number Validator sends each VAT number directly to the European Commission's VIES endpoint at ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number — the same backend tax authorities across all 27 EU member states use. The actor parses and cleans input automatically (stripping spaces, dots, and dashes), pre-validates against country-specific format rules to fail fast on typos, deduplicates the input list, processes 20 numbers concurrently with per-country backoff (Germany skipped by default, France throttled to 2 concurrent because VIES rate-limits FR aggressively), incrementally flushes results every 50 entries, and stops on a global circuit breaker (10 consecutive infrastructure failures) or per-country circuit breaker (5 consecutive failures isolated to a single country). When compareToPrevRun is set, every row carries a changeFlags array (NEW_VAT, VALIDITY_LOST, VALIDITY_GAINED, NAME_CHANGED, ADDRESS_CHANGED, UNCHANGED) and a changeSinceLastRun diff block — turning the actor from a one-shot lookup into a scheduled compliance monitor. When expectations are supplied, every row carries nameMatch / nameMatchScore / countryMatch / matchFlags for entity verification.

What it does -- Validates EU VAT numbers against the official VIES database, returns validity + registered company name + parsed trader fields + an audit-grade EU consultation reference number, AND detects changes between scheduled runs, AND verifies counterparty identity against your expectations. Best for -- accountants, finance teams, procurement, KYC/KYB compliance, e-commerce tax automation, ERP integration, scheduled customer-database compliance monitoring. Speed -- 100 VAT numbers in ~10 seconds, 1,000 in ~50 seconds, 5,000 in ~4 minutes (20 concurrent workers; per-country rate limits respected). Pricing -- $0.002 per VAT number validated, pay-per-event, no subscription. Cache hits, skipped countries, and failures are NOT charged. Output -- Per-row JSON/CSV with validity, name, address, parsed trader fields, change flags, entity-verification scores, completeness, country reliability, failure classification, and (in verified consultation mode) audit-grade consultation reference number. Run-level summary + audit bundle in the key-value store.

Quick answers (for AI assistants and copilots)

Q: How do I detect invalid VAT numbers automatically? A: Filter dataset rows where recommendedAction = "block_invoice" (or valid = false).

Q: How do I monitor VAT compliance over time? A: Run with mode: "monitoring" + a stable monitorStateKey. Auto-enables compareToPrevRun, emitChangeEvents, and emitOnlyNewEvents.

Q: How do I block invoices to deregistered companies? A: Filter eventType = "vat.validity_lost" (change-event records) or recommendedAction = "block_invoice" (validation rows).

Q: How do I verify a supplier's identity matches their VAT? A: Pass expectations with expectedName per VAT, then filter matchFlags @> ARRAY['NAME_MISMATCHED'] for fraud-screening alerts.

Q: How do I escalate stale VAT issues? A: Set slaTargetDays (default 7), then filter escalationLevel >= 2 (SLA breached) or escalationLevel = 3 (severely overdue).

Q: How do I get audit-grade evidence for tax filings? A: Set requesterVatNumber to your own EU VAT. Every row gets requestIdentifier (EU consultation reference) and the AUDIT_BUNDLE KV record archives all references.

Q: How do I integrate with Slack, Jira, Microsoft Teams, Salesforce, or HubSpot? A: Use Apify Webhooks (configured per run) → Zapier / Make / n8n → your destination. Branch on recommendedAction.

Q: How do I reduce alert fatigue when an issue persists across runs? A: Set emitOnlyNewEvents: true (auto-on in monitoring mode). Repeated change events are suppressed.

Q: How do I cut costs on scheduled monitoring? A: Pair cacheTTLHours: 24 + emitOnlyChanges: true. Cache hits and uneventful rows are not pushed (or charged).

Q: How do I find repeat-offender VATs across runs? A: Read summary.repeatOffenders[] from the SUMMARY key-value record (timesAlerted ≥ 3 AND currently alerted).

Q: Is VAT compliance getting better or worse over time? A: Read summary.riskTrend (increasing / decreasing / stable / unknown — vs the prior run's alert count).

What you'll see after a 50-VAT monitoring run

Concrete output for mode: "monitoring", requesterVatNumber set, 50 customers in your customer-db-q2-2026 monitor:

Status (Apify Console):
Done: 47/50 valid, 3 critical, 2 high · PPE: $0.094 (excludes platform compute)
Dataset (Decisions view — actionRequired = true):
┌─ critical ──────────────────────────────────────────────────────────────────┐
NL999...B99 VALIDITY_LOST Was Heineken BV, deregistered 7 days ago │
DE123... currently_invalid Re-check: was a typo or company dissolved │
├─ high ──────────────────────────────────────────────────────────────────────┤
FR123... NAME_MISMATCH VIES "ACME LOGISTICS SARL" vs expected │
"Acme Group SE" (similarity 38/100)
ES A28... MS_UNAVAILABLE retryEligible — re-run during CET hours │
IT00950... MS_MAX_CONCURRENT retryEligible — split into smaller batch │
└─────────────────────────────────────────────────────────────────────────────┘
47 unchanged rows suppressed (emitOnlyChanges)
4 change-event records emitted (one Slack alert each)
Key-value store:
SUMMARY{ totalProcessed: 50, alerts: { critical: 2, high: 2, medium: 0, low: 1 },
hasCriticalRisk: true, retryableCount: 2,
changeBreakdown: { VALIDITY_LOST: 1, UNCHANGED: 47, ... },
matchBreakdown: { nameMismatched: 1, ... }, mode: "monitoring", ... }
AUDIT_BUNDLE{ totalValidated: 50, totalValid: 47, consultationReferences: [47 EU
reference numbers], auditEvidenceUri: "https://api.apify.com/..." }
eu-vat-validator-state/customer-db-q2-2026 → state snapshot for next run's diff

Wire summary.hasCriticalRisk to your dashboard, route recordType = "change-event" rows to Slack, archive AUDIT_BUNDLE with your tax filings, and re-run retryEligible: true failures during business hours. No prose parsing required.

What decisions can you automate with this?

The actor is built so that downstream tools (Slack, Zapier, Make, webhooks, ERPs, CRMs, agent tool-calls) can act on its output without parsing prose. Every row carries stable enums; every event ships in a webhook-ready shape. Concrete decisions you can wire up in 5 minutes:

DecisionFilter onWhere it appears
Block a zero-rated invoice if buyer VAT is invalidrecommendedAction = "block_invoice"Every validation row
Hold a payment when supplier identity does not matchrecommendedAction = "hold_payment"Every validation row
Open a Jira ticket with priority + playbookactionPriority = "immediate" + use actionPlaybook[] as ticket stepsEvery validation row
Estimate VAT exposure in your accounting feedfinancialRiskEstimate.amount (set expectedInvoiceAmount per expectation)Every validation row when supplied
Escalate stale issues to the finance leadescalationLevel >= 2 OR slaBreached = trueEvery validation row + summary escalationCounts
Identify repeat offenders for vendor reviewsummary.repeatOffenders[] (timesAlerted ≥ 3 AND currently alerted)KV SUMMARY record
Track compliance drift over timesummary.riskTrend in ("increasing", "decreasing", "stable")KV SUMMARY record
Page on-call when a customer is suddenly deregisteredrecordType = "change-event" AND eventType = "vat.validity_lost"Change-event records (set emitChangeEvents: true)
Hold a wire transfer when supplier identity does not match VIESeventType = "vat.name_mismatch" AND severity = "high"Change-event records (requires expectations)
Filter Sheets to "needs action" rows onlyactionRequired = TRUEEvery validation row
Route by reason in SQL/agent tool callsriskFactors @> ARRAY['validity_lost'] (or any code)Every validation row
Route in Zapier/n8n (no array support)primaryRiskFactor = "validity_lost" (or any code)Every validation row
Re-run ONLY transient failures during business hoursretryEligible = trueFailures view
Suppress alert spam on persistent issuesemitOnlyNewEvents: true (auto-on in monitoring mode)Input
Send a quarterly compliance archive to auditorsAUDIT_BUNDLE key-value recordStorage tab
Trigger a critical-risk alertsummary.hasCriticalRisk = trueKV SUMMARY record
Track monitor healthsummary.alerts.{critical,high,medium,low} countsKV SUMMARY record

How EU VAT Number Validator works (mental model)

┌─────────────────────────────────────┐
INPUT: vatNumbers[] + mode + opts │
└──────────────────┬──────────────────┘
┌────────────────────────────────────────────────┐
1. Normalise + dedupe + country format pre-check│
└──────────────────┬─────────────────────────────┘
┌────────────────────────────────────────────────┐
2. Cache lookup (cacheTTLHours + state)
│ cache hit? → skip VIES, no charge │
└──────────────────┬─────────────────────────────┘
┌────────────────────────────────────────────────┐
3. VIES validation — 20 concurrent, per-country │
│ retries on 429/5xx/timeout, two circuit │
breakers (global + per-country)
└──────────────────┬─────────────────────────────┘
┌────────────────────────────────────────────────┐
4. Change detection — diff vs prior snapshot │
│ → changeFlags[], changeSinceLastRun │
└──────────────────┬─────────────────────────────┘
┌────────────────────────────────────────────────┐
5. Entity verification — match expectedName/
│ Country → matchFlags[], nameMatchScore │
└──────────────────┬─────────────────────────────┘
┌────────────────────────────────────────────────┐
6. Risk scoring + severity + group + explanation│
│ → riskScore, riskLevel, severity, group,
│ explanation[]
└──────────────────┬─────────────────────────────┘
┌────────────────────────┴────────────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────────┐
│ Validation rows │ │ Change-event records │ │ KV: SUMMARY + AUDIT_BUNDLE
(or suppressed │ (one per material │ (run-level rollup +
in changes-only │ │ change, webhook- │ │ drop-in audit archive)
│ mode) │ │ shaped) │ │ │
└──────────────────┘ └──────────────────────┘ └──────────────────────────┘
  • Skip stages by leaving the relevant inputs unset — validation always runs; everything else is opt-in (or auto-on via the mode preset).
  • mode: "monitoring" auto-enables stages 2 / 4 / 6 + change-event emission.

Compliance operations lifecycle

Designed specifically for ongoing VAT compliance monitoring across scheduled runs.

The actor closes the full SIGNAL → DECISION → ESCALATION → RESOLUTION loop across scheduled runs. State lives in a named KV store (eu-vat-validator-state) keyed by your monitorStateKey, so each run knows what fired before and what's still open.

RUN 1 — Monday RUN 2 — Wednesday RUN 3 — Next Monday
───────────────── ───────────────── ─────────────────────
NL999 detected as INVALID NL999 still INVALID NL999 still INVALID
↓ ↓ ↓
firstAlertedAt = Mon firstAlertedAt = Mon (sticky) firstAlertedAt = Mon (sticky)
daysOpen = 0 daysOpen = 2 daysOpen = 7
slaBreached = false slaBreached = false slaBreached = TRUE
escalationLevel = 1 escalationLevel = 1 escalationLevel = 2
recommendedAction = block_invoice (immediate priority — same across runs)
actionPlaybook[] = [4 ticket-ready steps]
businessImpact = "If you issue a zero-rated B2B invoice... liable for full VAT under member-state law"
emitChangeEvents fires ONCE emitOnlyNewEvents emitOnlyNewEvents
(VALIDITY_LOST event record) suppresses repeat suppresses repeat
→ escalationLevel jump alerts
your finance lead via the
summary.escalationCounts."2"
counter
→ repeatOffenders[] in
summary now lists NL999
(timesAlerted ≥ 3)
When the supplier fixes their VAT and you re-run:
changeFlags = ["VALIDITY_GAINED"]
firstAlertedAt = null (issue resolved)
recommendedAction = "none"
daysOpen = null
riskTrend = "decreasing"

Wire summary.escalationCounts to your dashboard, recommendedAction to your ticket creator, actionPlaybook to your ticket body, and summary.repeatOffenders to your vendor-review report. The actor handles state, lifecycle, and escalation; your downstream tools handle resolution.

In practice, this changes how a finance or compliance team works. Manual VAT checking — log into the VIES portal, enter each number, copy the result into a spreadsheet, remember to recheck monthly — gets replaced by a scheduled run that surfaces only what changed. A new deregistration becomes a Slack alert with the recommended action and a 4-step playbook ready to paste into a Jira ticket. A name mismatch on a supplier becomes a hold-payment trigger before the wire goes out. An SLA breach becomes a dashboard tile that nobody has to manually update. The team stops watching VIES and starts acting on what VIES tells them.

How to wire alerts to Slack, Jira, Teams, Zapier, Make, n8n

This actor produces structured signals — every row carries recommendedAction, actionPriority, actionPlaybook[], severity, escalationLevel, and recordType: 'change-event' records for material changes. The actor itself does NOT send Slack messages, create Jira tickets, or call your email provider. That's deliberate — and the right pattern for a compliance tool.

Use Apify Webhooks (configured per run on the Apify platform, not in this actor's input) to deliver every dataset row to any URL. From there, route via your existing automation layer:

DestinationHow
SlackApify Webhook → Zapier/Make/n8n → Slack incoming webhook. Filter severity = "high" OR recommendedAction = "block_invoice".
Microsoft TeamsSame pattern — Teams accepts incoming webhooks identically. Filter on escalationLevel >= 2 for SLA-breach alerts.
Jira / Linear / GitHub IssuesZapier or n8n "Create issue" step. Use actionPlaybook[] as the body, actionPriority (immediate/high/normal/low) as Priority, recommendedAction as a label.
PagerDutyFilter escalationLevel >= 2 AND recommendedAction = "block_invoice" to fire an incident.
Email (SendGrid / Postmark / SES / Mailgun)Zapier email step OR your provider's HTTP API directly via Make/n8n.
n8n / Make / ZapierBranch on recommendedAction for routing; pass actionPlaybook[] as the action template.
Salesforce / HubSpot CRMUpdate the supplier record with riskLevel + escalationLevel; create a task when actionRequired = true.
Webhooks generallyAny URL accepting POST. Apify retries, signs, and audit-logs the delivery for you.

Why this actor doesn't ship native Slack/Jira/email senders

  • Flexibility. Every team's stack is different — building, secret-managing, and maintaining clients for Slack, Teams, Jira, Linear, GitHub Issues, ServiceNow, Asana, ClickUp, Salesforce, HubSpot, plus 5+ email providers is a separate product. Apify Webhooks deliver the same payload to ALL of them via your chosen routing layer.
  • Safety. A compliance tool firing destructive actions (tickets, emails, payments-on-hold) needs auth, retries, rate limits, dry-run rehearsal, and per-integration audit trails. Routing through your existing automation layer (which already has those guarantees) is safer than us reinventing it inside the actor.
  • Auditability. When a Slack alert or Jira ticket goes out, your finance lead asks "who sent this and when?". With Apify Webhook → Zapier, the audit trail lives in ONE system you already know. With inline senders, the trail splits between Apify logs and Zapier logs — confusing for compliance reviews.

Net: the actor is the deterministic, audit-safe, schema-stable SIGNAL generator. Your existing automation stack is the RESPONSE layer. Together they're more flexible AND safer than a monolithic actor that ships its own senders.

See Apify Webhooks docs for the platform-level setup.

What's new in v3 (compliance operations system)

Unlike commercial VAT validators that ship one capability per product (validation OR monitoring OR audit OR compliance workflows), this actor combines all four layers in one schema-stable pipeline — with deterministic enums an automation system can branch on without parsing prose.

LayerCapability
Action enginerecommendedAction enum (block_invoice / hold_payment / request_certificate / review / retry_later / monitor_only / none) + actionPriority (immediate / high / normal / low) + actionPlaybook[] (2–4 steps usable verbatim in tickets) — derived deterministically from primaryRiskFactor + failureType
SLA + escalationslaTargetDays input; per-row firstAlertedAt (sticky), daysOpen, slaBreached, escalationLevel (0–3), escalationReason; summary.slaBreaches + summary.escalationCounts for ops queues
Business impactbusinessImpact plain-English board-level explanation per primary risk factor; optional financialRiskEstimate (21% conservative EU VAT rate) when expectedInvoiceAmount supplied per expectation
Lifecycle metadataRead-only resolution signals — firstSeenAt, daysSinceFirstSeen, timesSeen, timesAlerted per row; summary.repeatOffenders (top 10 VATs alerted in 3+ runs and still alerted)
Portfolio intelligencesummary.topRiskFactors (top 5 by count), summary.repeatOffenders, summary.riskTrend (increasing / decreasing / stable / unknown — vs prior run's alert count)
Triage viewNew dataset view sorting alerted rows by escalation level + days open — drop-in for finance ops triage queues

What's in v2 (foundation)

LayerCapability
MonitoringCross-run change detection (NEW_VAT / VALIDITY_LOST / VALIDITY_GAINED / NAME_CHANGED / ADDRESS_CHANGED / UNCHANGED), per-row diff vs prior state, cache TTL skips re-validating recent VATs (and is NOT charged), emitOnlyChanges mode suppresses uneventful rows, emitOnlyNewEvents suppresses repeat alerts
Event layerOptional recordType: 'change-event' records — one per material change, webhook-shaped (eventType + severity + context). Repeat-event suppression via emitOnlyNewEvents (auto-on in monitoring mode) prevents Slack/Zapier alert fatigue. Branch downstream automation on recordType instead of scanning every row
AuditVerified consultation mode → EU consultation reference numbers per row; AUDIT_BUNDLE KV record drop-in for tax archives; audit mode preset
VerificationPer-VAT expectations with expectedName / expectedCountry → match scoring (Levenshtein), nameMatch / nameMatchScore / countryMatch / matchFlags enum
Risk + decision layerriskScore (0-1), riskLevel (critical/high/medium/low), severity, group (changed/new/unchanged/invalid/failed), actionRequired boolean, riskFactors[] machine codes, explanation[] plain-English per-row; summary.alerts + summary.hasCriticalRisk for dashboards
IntelligencePer-row dataCompleteness (0..1) + missingFields[], countryReliability hint, failureType enum + plain-English recommendation, retryEligible boolean
ValidationDirect VIES REST, 28-country format pre-check, per-country rate-limited concurrency, retries with backoff, two-tier circuit breaker (global + per-country)
UXmode preset (auto / quick / audit / monitoring) — set the mode, get sensible defaults; explicit fields always win; expectations accepts array OR object form
OutputrecordType discriminator on every row; 9 dataset views (Overview / Risk / Decisions / Triage / Audit / Trader / Changes / Verification / Failures / Change Events)

Common automation patterns

Common one-line filter → action mappings — paste these into Zapier / Make / n8n / SQL / agent tool calls:

SignalRecommended actionRouting destination
recommendedAction = "block_invoice"Stop pending zero-rated invoiceFinance ops Slack channel + accounting freeze
recommendedAction = "hold_payment"Pause in-flight paymentAP queue + supplier notification
recommendedAction = "request_certificate"Email supplier for updated VAT certSupplier outreach automation
recommendedAction = "retry_later"Re-run during business hoursApify Schedules (next CET morning)
escalationLevel >= 2Escalate to finance leadPagerDuty + email + Slack mention
escalationLevel = 3Severely overdue — escalate to compliance directorPagerDuty critical incident
summary.hasCriticalRisk = trueTrigger compliance review meetingCalendar block + dashboard alert
summary.repeatOffenders[] not emptyAdd to vendor-review reportQuarterly procurement review queue
eventType = "vat.validity_lost"Alert account owner immediatelyCRM update + sales-rep notification
eventType = "vat.name_mismatch"Pause + investigate fraud signalCompliance ticket + account-owner review
recordType = "change-event" AND severity = "high"Slack alert with actionPlaybook as body#compliance-alerts
actionRequired = TRUESpreadsheet filter to "needs review"Google Sheets / Excel auto-filter

Problem → solution

If your problem is...Use this filter / field
You issued a zero-rated invoice and now the buyer's VAT is invalidrecommendedAction = "block_invoice" (or valid = false + archive requestIdentifier for audit evidence)
Supplier's company name doesn't match what VIES sayseventType = "vat.name_mismatch" (set expectations first to enable scoring)
Customer was valid last quarter but is now deregisteredchangeFlags @> ARRAY['VALIDITY_LOST'] (or eventType = "vat.validity_lost")
Country VIES node went down mid-runfailureType = "no-data-temp" AND retryEligible = true
Need legal proof you verified the buyer before issuing the invoiceSet requesterVatNumber for verified consultation mode → every row carries requestIdentifier (audit evidence under VAT Directive Art 138) + the AUDIT_BUNDLE KV record archives all references
Compliance SLA is 7 days, want to escalate stale issuesslaTargetDays: 7 input + filter escalationLevel >= 2
Don't want to spam Slack on the same persistent issue every runSet emitOnlyNewEvents: true (auto-on in monitoring mode)
Run this daily but only changes mattermode: "monitoring" + cacheTTLHours: 24 + emitOnlyChanges: true (no-cost no-noise loop)
Need to flag VATs that have been problematic across many runssummary.repeatOffenders[] (timesAlerted ≥ 3 AND currently alerted)
Track whether compliance is getting better or worsesummary.riskTrend (increasing / decreasing / stable / unknown)
Fraud-screening: catch suppliers operating under different legal entityeventType = "vat.country_mismatch"
Estimate VAT liability exposure on a high-risk rowProvide expectedInvoiceAmount per expectation; row gets financialRiskEstimate.amount (indicative — not tax advice)

What data can you extract with EU VAT validation?

Data PointSourceExample
VAT NumberInput (cleaned)FR40303265045
Country CodeParsed from inputFR
Validity StatusVIES API responsetrue
Company NameVIES member state dataTOTAL ENERGIES SE
Registered AddressVIES member state data2 PLACE JEAN MILLIER, LA DEFENSE 6, 92400 COURBEVOIE
Trader Name / Street / Postal Code / City / Company TypeVIES parsed fieldsTOTAL ENERGIES SE / 2 PLACE JEAN MILLIER / 92400 / COURBEVOIE / SE
Request DateVIES API timestamp2026-05-01T10:22:16.000+01:00
VIES Consultation Reference (audit)VIES requestIdentifierWAPIAAAAW7QwsB4n
Change Flags (monitoring mode)Cross-run diff["VALIDITY_LOST", "NAME_CHANGED"]
Change Diff (monitoring mode)Cross-run state{previousValid: true, previousName: "X", daysSinceLastSeen: 7}
Name Match Score (verification mode)Levenshtein similarity94
Match Flags (verification mode)Entity verification["NAME_MATCHED", "COUNTRY_MATCHED"]
Data CompletenessComputed0.71 (5 of 7 optional fields populated)
Country ReliabilityStatic hintlow (DE), medium (FR/IT/ES), high (others)
Failure Type (classified)Actor logicno-data-temp, invalid-input, rate-limited
Recommendation (next step)Actor logicRe-run during European business hours (08:00–17:00 CET).

Why use EU VAT Number Validator?

If you're choosing an EU VAT validation API: VIES is the source of truth, but this actor turns it into decision-ready, automation-friendly output — with monitoring, escalation, and audit-grade evidence layered on top of the same data.

Validating VAT numbers manually on the European Commission's VIES portal means entering them one at a time, waiting for each response, copy-pasting the results into a spreadsheet, and then -- if you need audit evidence -- separately downloading and archiving the consultation receipt for each number. For 50 numbers, that takes over an hour. For 500, it takes a full working day. And the portal does not tell you what changed since the last time you checked.

EU VAT Number Validator automates all of that and adds the layers VIES alone does not give you:

  1. Speed at scale -- 20-way concurrency with per-country rate limiting validates 1,000 numbers in about 50 seconds.
  2. Audit-grade output -- supply your own VAT (requesterVatNumber) and VIES returns a stable consultation reference number (the legal proof under VAT Directive Art 138). The actor preserves it on every row AND ships an AUDIT_BUNDLE record in the key-value store as a drop-in archive for tax filings.
  3. Stateful change detection -- enable compareToPrevRun and every scheduled run tells you exactly which counterparties were deregistered, which had their company name change, and which are entirely new -- without you writing diff logic.
  4. Entity verification -- supply expectations with the company name you THINK each VAT belongs to, and the actor scores the match (Levenshtein similarity) so you catch mismatches between supplier-claimed identity and VIES-recorded identity.
  5. Compliance-ready failure handling -- every failure carries a stable failureType enum and a plain-English recommendation, so downstream automation routes by type instead of parsing error strings.
  • Scheduling -- run daily, weekly, or monthly to catch VAT deregistrations in your customer or supplier database; pair with compareToPrevRun to get the diff for free
  • Cache TTL -- in monitoring mode, opt into cacheTTLHours and the actor skips VATs validated within that window (cache hits are NOT charged)
  • API access -- trigger validation runs from Python, JavaScript, or any HTTP client for real-time integration
  • Built-in retries -- automatic exponential backoff on VIES rate limits, HTTP 5xx, and network errors (3 retries, increasing delay)
  • Two-tier circuit breaker -- global breaker stops the run on 10 consecutive infrastructure failures (VIES outage), per-country breaker isolates a single bad country (5 consecutive failures) without aborting the rest

Why not just use the VIES portal directly?

The European Commission's VIES portal answers ONE question: "is this VAT valid right now?". It does not:

  • Diff against last week's results (deregistrations are silent — VIES has no historical API)
  • Tell you what action to take (block invoice? hold payment? just monitor?)
  • Track issue age or SLA-breach escalation across scheduled runs
  • Generate machine-readable consultation reference numbers via the UI (only via the verified consultation API the actor wraps)
  • Bulk-validate without manual entry of each number, one at a time
  • Match VIES-returned names against your expected names (entity verification / fraud screening)
  • Survive temporary node outages without manual retries
  • Aggregate top risk factors, repeat offenders, or risk trends across runs

This actor adds all eight layers on top of the same official VIES REST API the portal uses. Same VIES data — but with automation, monitoring, and compliance workflows layered on top.

Unlike custom-building this on top of the raw VIES API, the actor handles state management, retry logic, two-tier circuit breakers, country format pre-validation, change detection, audit-bundle generation, repeat-offender tracking, and SLA escalation out of the box — components a typical internal-build estimate runs at 4–8 engineer-weeks.

Features

  • Official VIES API -- queries the European Commission's production REST endpoint, the same backend national tax authorities use across all EU member states. No proxy, no intermediary.
  • All 28 country codes supported -- AT, BE, BG, CY, CZ, DE, DK, EE, EL, ES, FI, FR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK, plus XI (Northern Ireland post-Brexit)
  • Mode presets -- pick auto (recommended — resolved from your input), quick (fastest), audit (preserves consultation reference numbers — requires requesterVatNumber), or monitoring (cross-run change detection on by default). Explicit fields always override preset defaults.
  • Verified consultation mode (audit-grade) -- supply your own VAT in requesterVatNumber and VIES returns a requestIdentifier for every lookup. EU tax authorities accept this consultation reference as proof of verification under VAT Directive Article 138. Without it, the VIES check is informational only.
  • Cross-run change detection -- enable compareToPrevRun to compare results against the previous run's state (stored in named KV store eu-vat-validator-state, keyed by monitorStateKey). Every row carries a changeFlags array (NEW_VAT, VALIDITY_LOST, VALIDITY_GAINED, NAME_CHANGED, ADDRESS_CHANGED, UNCHANGED) and a changeSinceLastRun block (previous values + daysSinceLastSeen + firstSeenAt + lastSeenAt). State is FIFO-capped at 50,000 entries. First run for a key is NEW_VAT on every row.
  • Cache TTL (cost saver in monitoring mode) -- set cacheTTLHours together with compareToPrevRun and VATs validated within that window are returned from state instead of hitting VIES. Cache hits are flagged with cacheHit: true and are NOT charged.
  • Entity verification -- supply expectations (map of VAT → { expectedName, expectedCountry }) and every row gets nameMatch (true if Levenshtein similarity ≥ 90), nameMatchScore (0-100), countryMatch, and a matchFlags enum (NAME_MATCHED / NAME_PARTIAL_MATCH / NAME_MISMATCHED / COUNTRY_MATCHED / COUNTRY_MISMATCHED / NO_EXPECTED_DATA). Catches counterparty identity mismatches before you wire money.
  • Trader field parsing -- VIES returns traderName, traderStreet, traderPostalCode, traderCity, and traderCompanyType as separate fields. The actor preserves all five so KYB and entity-matching pipelines can match on city / postcode without fragile address parsing.
  • 20-way concurrent processing -- 20 parallel workers with per-country rate limits (FR capped at 2 concurrent because VIES throttles FR aggressively, default 4 elsewhere). 1,000 numbers in ~50 seconds.
  • Slow-country sort -- France and Germany are sorted last so fast countries return results first; results flush incrementally every 50 numbers, so a slow DE queue at the end never blocks earlier rows from reaching your dataset.
  • Incremental flush + per-item PPE charging -- results are pushed to the dataset every 50 entries (no data loss on abort), and each result is charged individually after pushData (so the spending limit cuts the run cleanly mid-batch instead of leaking up to 49 unbilled rows). Cache hits, skipped countries, and failures are NOT charged.
  • Country-specific format pre-validation -- 28 country regex patterns (NL: 9 digits + B + 2 digits, DE: 9 digits, FR: 2 alphanumeric + 9 digits, etc.) reject obvious typos with INVALID_COUNTRY_FORMAT before they reach VIES. Saves money and reduces VIES load. Disable with validateFormatLocally: false.
  • Country reliability hints -- every row carries countryReliability (high / medium / low) based on documented VIES uptime + rate-limiting behaviour. Lets downstream filtering deprioritise rows from low-reliability countries.
  • Data completeness scoring -- every successful row carries dataCompleteness (0.0-1.0) + missingFields[] so downstream automation can filter on row quality (e.g. only forward rows with dataCompleteness >= 0.8).
  • Input deduplication -- duplicate VAT numbers (case-insensitive, normalised) are removed before processing. Default on; disable with deduplicate: false if you need one output row per input row.
  • Germany opt-in handling -- DE's VIES node is chronically unreliable (40-60% uptime); DE numbers are skipped by default with failureType: 'no-data-permanent' and a clear recommendation. Enable includeUnreliableCountries to attempt anyway.
  • Two-tier circuit breaker -- global breaker trips on 10 consecutive infrastructure failures across the whole run (VIES outage); per-country breaker isolates a single bad country after 5 consecutive failures without aborting the run. Stops a bad day at the European Commission from burning your spending limit.
  • Exponential backoff retries -- 3 retries with increasing delays (3s, 6s, 9s) on MS_MAX_CONCURRENT_REQ, SERVICE_UNAVAILABLE, HTTP 5xx, network errors, and AbortError timeouts.
  • 30-second per-call timeout -- every VIES request has AbortSignal.timeout(30_000) so a hung connection cannot block a worker indefinitely.
  • Stable failure classification -- every failure row carries a failureType enum (invalid-input / no-data-temp / no-data-permanent / rate-limited / network-error / vies-error) and a plain-English recommendation.
  • recordType discriminator -- every row carries recordType: 'validation' | 'error' | 'fatal' for downstream SQL / Sheets / agent tool filtering.
  • Run summary + audit bundle in key-value store -- the run-level breakdown (totals, by country, error histogram, change-flag counts, match counts, cache hits, charged events, requester VAT, start/end timestamps, resolved mode) is written to SUMMARY. In any run with requesterVatNumber, an AUDIT_BUNDLE record is also written with a compact {runId, requesterVatNumber, totals, consultationReferences[], auditEvidenceUri} shape — drop-in for compliance archives.
  • Cost transparency -- PPE price logged at run start, large-batch warning at >=500 numbers with worst-case cost estimate, running PPE total surfaced in progress + final status messages, "stopped at spending limit" path also shows total charges. Cache hits + skipped countries + failure rows are NOT charged.
  • Pay-per-event pricing -- $0.002 per VAT number validated, with spending-limit support to cap costs per run. Charges fire only after the result is in the dataset AND only on actual VIES validations.
  • Lightweight footprint -- runs on 128-512 MB; the bottleneck is VIES response time, not compute.

Use cases for EU VAT number validation

Best for invoice compliance and zero-rating verification (audit-grade)

Finance teams issuing intra-community invoices under EU VAT Directive Article 138 need to verify that the buyer holds a valid VAT registration before applying zero-rate treatment, AND need to retain proof of that verification for tax audits. Verified consultation mode (requesterVatNumber) returns an EU consultation reference number for every lookup -- the legal evidence tax authorities accept. The AUDIT_BUNDLE KV record gives you a drop-in archive per run.

Best for scheduled compliance monitoring

Compliance teams running quarterly customer-database health checks want to know exactly which counterparties were deregistered or renamed since the last run -- not just the current state. Schedule the actor weekly or monthly with compareToPrevRun: true and a stable monitorStateKey. Every row tells you what changed; the run summary includes a change-flag breakdown for dashboards.

Best for supplier onboarding and procurement (entity verification)

Built-in supplier verification: match VAT records against expected company names with scoring. Procurement teams adding new vendors need to confirm that the company on the invoice is actually the one VIES has registered against the supplied VAT. Pass expectations: { "FR40303265045": { "expectedName": "Total Energies SE", "expectedCountry": "FR" } } and the actor returns nameMatch + nameMatchScore per row -- catches typos, name variations, and outright fraud before the wire transfer goes out.

Best for KYC/KYB and AML compliance pipelines

Compliance officers building automated Know Your Business workflows need structured trader fields (name, street, postcode, city, company type) for entity matching against sanctions screens. The actor preserves all five trader components separately, plus a dataCompleteness score so downstream filters can drop low-quality rows.

Best for e-commerce B2B tax automation

Online sellers processing B2B orders within the EU need to validate buyer VAT numbers in real time. The actor integrates via API, validates a single number in under 2 seconds, and returns a deterministic failureType enum your checkout flow can route on without parsing strings.

Best for M&A due diligence across EU jurisdictions

Deal teams evaluating acquisition targets with operations across multiple EU countries need to verify VAT registrations for the parent entity and every subsidiary. Mixed-country batches return per-country breakdowns in the run summary, and verified consultation mode gives you audit-grade proof for every entity in the data room.

When to use EU VAT Number Validator

Best for:

  • Batch validation of 10-10,000 VAT numbers in a single run
  • Scheduled compliance monitoring with cross-run change detection (deregistrations, renames, address changes)
  • Supplier onboarding with entity verification against expected names
  • Audit preparation requiring timestamped, EU-recognised consultation reference numbers (use requesterVatNumber)
  • Automated KYC/onboarding pipelines that need programmatic VIES access via API

What this actor does NOT do:

  • It does not validate UK (GB) VAT numbers -- standard GB numbers left the VIES system after Brexit; only Northern Ireland (XI) numbers are supported. For UK validation use HMRC's separate VAT API or UK Companies House Search for entity verification.
  • It is not a real-time single-call API under 100ms -- VIES typically responds in 500ms-2s per number. For sub-second checkout flows, cache VIES results locally with a TTL.
  • It does not guarantee German (DE) results -- Germany's VIES node is chronically offline. DE is skipped by default; enable opt-in but expect intermittent failures even with retries.
  • It does not verify against national registries -- VIES confirms the VAT registration is active in the member state's database. It does not cross-check the company name against Companies House, BvD, or other corporate registries. Pair with OpenCorporates Search or GLEIF LEI Lookup for that. Use the actor's entity verification (expectations) for VIES-side name matching only.
  • It does not return historical data from VIES -- VIES only confirms current registration status. It does not provide a record of when a VAT number was registered or deregistered. The actor's cross-run change detection (compareToPrevRun) reconstructs THIS over scheduled runs, but VIES itself has no historical API.
  • It does not bypass VIES rate limits -- the actor respects per-country concurrency caps because VIES rate-limits aggressively. Very large concurrent runs may still encounter MS_MAX_CONCURRENT_REQ; the two-tier circuit breaker stops the run (or the affected country) so a VIES outage does not burn your budget.
  • It does not run national checksum validation -- the actor pre-validates against country-specific length and character patterns but does not run national checksum algorithms (e.g., the Spanish DNI algorithm). VIES is the source of truth for validity.

How to validate EU VAT numbers in bulk

  1. Enter your VAT numbers -- Add them to the VAT Numbers list, one per line. Use the format: 2-letter country code followed by the number (e.g., FR40303265045, NL004495445B01). Spaces, dots, and dashes are stripped automatically. Up to 10,000 numbers per run.
  2. Pick a mode -- auto (default) is the right pick for most users. Use audit if you need consultation reference numbers (and supply requesterVatNumber). Use monitoring for scheduled compliance checks (turns on compareToPrevRun and pairs well with a stable monitorStateKey).
  3. (Optional) Add audit reference -- If you need EU consultation reference numbers for tax audits, enter your company's VAT in the "Your VAT number" field. Every result will then include a requestIdentifier you can archive as proof.
  4. (Optional) Add expectations -- For entity verification, supply expectations as { "FR40303265045": { "expectedName": "Total Energies SE" } }. Every row will include a match score.
  5. Run the actor -- Click "Start" and wait. 100 numbers complete in about 10 seconds. 1,000 numbers in about 50 seconds.
  6. Download results -- Go to the Dataset tab. Six views are available: Overview, Audit Evidence, Parsed Trader Fields, Changes Since Last Run, Entity Verification, Failures Only.
  7. Read the run summary + audit bundle -- The Storage > Key-value store tab has a SUMMARY record with country breakdown, error histogram, change-flag counts, totals. If you ran in verified mode, an AUDIT_BUNDLE record is also there as a drop-in compliance archive.

Input parameters

ParameterTypeRequiredDefaultDescription
vatNumbersArray of stringsYes["NL004495445B01"]List of EU VAT numbers to validate (max 10,000). Country prefix required. Spaces, dots, dashes stripped.
modeSelectNoautoWorkflow preset: auto / quick / audit / monitoring. Explicit fields always win over preset defaults.
requesterVatNumberStringNo--Your own EU VAT. When set, VIES returns a consultation reference number (audit evidence under VAT Directive Art 138).
compareToPrevRunBooleanNofalse (auto-on in monitoring mode)Compare against the previous run's state. Emits changeFlags + changeSinceLastRun per row.
monitorStateKeyStringNoauto-derivedStable identifier for the change-detection state snapshot. Reuse across scheduled runs.
emitChangeEventsBooleanNofalse (auto-on in monitoring mode)Emit additional recordType: 'change-event' rows for every material change. Webhook-shaped, NOT charged.
emitOnlyChangesBooleanNofalseSuppress uneventful validation rows in monitoring mode. VIES calls still happen and bill normally — pair with cacheTTLHours for true no-cost monitoring.
emitOnlyNewEventsBooleanNofalse (auto-on in monitoring mode)When set with emitChangeEvents, only emit a change-event the first run a (vatNumber, eventType) pair appears. Prevents Slack/Zapier alert fatigue when an issue persists.
cacheTTLHoursInteger (1-720)No--When set with compareToPrevRun, skip VIES re-validation for VATs checked within this many hours. Cache hits are NOT charged.
slaTargetDaysInteger (1-365)No7SLA target in days. Drives daysOpen / slaBreached / escalationLevel / escalationReason per row. Only meaningful with compareToPrevRun.
expectationsArray OR ObjectNo--Entity verification: array of {vatNumber, expectedName, expectedCountry, expectedInvoiceAmount, expectedCurrency} (recommended) OR object map. Emits match scores per row. When expectedInvoiceAmount is supplied AND the row gets block_invoice / hold_payment, the actor computes financialRiskEstimate.
deduplicateBooleanNotrueSkip duplicate VAT numbers (matched after normalisation).
validateFormatLocallyBooleanNotruePre-validate against country regex; numbers that fail return INVALID_COUNTRY_FORMAT without hitting VIES.
includeUnreliableCountriesBooleanNofalseAttempt validation for Germany (DE), whose VIES node is frequently offline.
includeAddressBooleanNotrueInclude name, address, and parsed trader fields. Disable for validity-only privacy-sensitive workflows.

Input examples

Quick validity check (auto mode resolves to quick):

{
"vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"]
}

Audit-grade verified consultation (auto mode resolves to audit):

{
"vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"],
"requesterVatNumber": "NL123456789B01"
}

Scheduled compliance monitoring (auto mode resolves to monitoring — emits change events + suppresses noise + caches):

{
"vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"],
"compareToPrevRun": true,
"monitorStateKey": "customer-db-q2-2026",
"cacheTTLHours": 24,
"emitOnlyChanges": true
}

The monitoring mode auto-enables emitChangeEvents: true so your webhook receiver sees one change-event row per material change. Pair emitOnlyChanges + cacheTTLHours for the canonical no-noise no-cost monitoring loop.

Entity verification on supplier onboarding (array form — recommended):

{
"vatNumbers": ["FR40303265045", "NL004495445B01"],
"expectations": [
{ "vatNumber": "FR40303265045", "expectedName": "Total Energies SE", "expectedCountry": "FR" },
{ "vatNumber": "NL004495445B01", "expectedName": "Heineken NV" }
]
}

Entity verification (object form — back-compat, still supported):

{
"vatNumbers": ["FR40303265045", "NL004495445B01"],
"expectations": {
"FR40303265045": { "expectedName": "Total Energies SE", "expectedCountry": "FR" },
"NL004495445B01": { "expectedName": "Heineken NV" }
}
}

Full compliance pipeline (audit + monitoring + verification):

{
"vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"],
"mode": "monitoring",
"requesterVatNumber": "NL123456789B01",
"compareToPrevRun": true,
"monitorStateKey": "customer-db-q2-2026",
"cacheTTLHours": 12,
"expectations": {
"FR40303265045": { "expectedName": "Total Energies SE" }
}
}

Input tips

  • Always include the country code prefix -- use DE129273398, not 129273398.
  • Use EL for Greece -- the VIES system uses EL (Ellada), not GR.
  • Use XI for Northern Ireland -- standard UK (GB) numbers are not supported post-Brexit.
  • For audit evidence, always set requesterVatNumber -- without it, VIES returns the result but no consultation reference. The reference is the legal proof, not the result itself.
  • For scheduled monitoring, set a stable monitorStateKey -- reuse it across scheduled runs so change detection compares against the right snapshot. Auto-derived keys depend on the input set; explicit keys are stable.
  • Use cacheTTLHours to control VIES load + cost -- in monitoring mode with daily schedules, set cacheTTLHours: 24 to skip VATs validated yesterday. Cache hits are free.
  • Batch in one run -- processing 1,000 numbers in one run is faster and cheaper than 1,000 individual runs.
  • Skip DE unless you need it -- leave includeUnreliableCountries off unless German validation is required.

Output example

Successful validation (verified consultation + monitoring + verification):

{
"recordType": "validation",
"vatNumber": "FR40303265045",
"countryCode": "FR",
"number": "40303265045",
"valid": true,
"name": "TOTAL ENERGIES SE",
"address": "2 PLACE JEAN MILLIER\nLA DEFENSE 6\n92400 COURBEVOIE",
"traderName": "TOTAL ENERGIES SE",
"traderStreet": "2 PLACE JEAN MILLIER",
"traderPostalCode": "92400",
"traderCity": "COURBEVOIE",
"traderCompanyType": "SE",
"requestDate": "2026-05-01T10:22:16.000+01:00",
"requestIdentifier": "WAPIAAAAW7QwsB4n",
"failureType": null,
"recommendation": null,
"dataCompleteness": 1.0,
"missingFields": [],
"countryReliability": "medium",
"changeFlags": ["UNCHANGED"],
"changeSinceLastRun": {
"previousValid": true,
"previousName": "TOTAL ENERGIES SE",
"previousAddress": "2 PLACE JEAN MILLIER\nLA DEFENSE 6\n92400 COURBEVOIE",
"firstSeenAt": "2026-04-01T08:14:11.000Z",
"lastSeenAt": "2026-04-24T08:14:11.000Z",
"daysSinceLastSeen": 7
},
"cacheHit": false,
"nameMatch": true,
"nameMatchScore": 100,
"countryMatch": true,
"matchFlags": ["NAME_MATCHED", "COUNTRY_MATCHED"],
"severity": null,
"riskScore": 0.02,
"riskLevel": "low",
"group": "unchanged",
"explanation": [],
"retryEligible": false,
"riskFactors": ["medium_reliability_country"],
"primaryRiskFactor": "medium_reliability_country",
"actionRequired": false
}

Cross-run alert (deregistered since last run):

{
"recordType": "validation",
"vatNumber": "NL999999999B99",
"countryCode": "NL",
"valid": false,
"name": null,
"address": null,
"requestDate": "2026-05-01T10:22:18.000+01:00",
"changeFlags": ["VALIDITY_LOST"],
"changeSinceLastRun": {
"previousValid": true,
"previousName": "EXAMPLE BV",
"previousAddress": "AMSTERDAM",
"firstSeenAt": "2026-01-01T08:00:00.000Z",
"lastSeenAt": "2026-04-24T08:14:11.000Z",
"daysSinceLastSeen": 7
},
"countryReliability": "high",
"dataCompleteness": 0,
"missingFields": ["name", "address", "traderName", "traderStreet", "traderPostalCode", "traderCity", "traderCompanyType"],
"severity": "critical",
"riskScore": 1.0,
"riskLevel": "critical",
"group": "invalid",
"riskFactors": ["validity_lost", "currently_invalid", "very_low_completeness"],
"primaryRiskFactor": "validity_lost",
"actionRequired": true,
"explanation": [
"VIES reports this VAT as not currently registered.",
"VAT became invalid since the previous run (last seen 7 days ago).",
"Sparse data from VIES (0% of optional fields populated)."
]
}

Entity-verification mismatch (supplier identity does not match VIES):

{
"recordType": "validation",
"vatNumber": "FR12345678901",
"valid": true,
"name": "ACME LOGISTICS SARL",
"nameMatch": false,
"nameMatchScore": 38,
"matchFlags": ["NAME_MISMATCHED", "COUNTRY_MATCHED"],
"countryReliability": "medium"
}

Failure with classification + recommendation:

{
"recordType": "error",
"vatNumber": "ES A28015865",
"countryCode": "ES",
"valid": false,
"error": "MS_UNAVAILABLE",
"failureType": "no-data-temp",
"recommendation": "The country VIES node was offline. Re-run failed numbers during European business hours (08:00–17:00 CET).",
"countryReliability": "medium",
"retryEligible": true,
"severity": "low",
"riskScore": 0.1,
"riskLevel": "low",
"group": "failed",
"explanation": ["The country VIES node was offline. Re-run failed numbers during European business hours (08:00–17:00 CET)."]
}

Change-event record (deregistration alert — drop into Slack/Zapier/webhook):

{
"recordType": "change-event",
"eventType": "vat.validity_lost",
"vatNumber": "NL999999999B99",
"countryCode": "NL",
"severity": "critical",
"timestamp": "2026-05-01T10:22:18.000Z",
"context": {
"previousValid": true,
"currentValid": false,
"previousName": "EXAMPLE BV",
"currentName": null,
"previousAddress": "AMSTERDAM",
"currentAddress": null,
"daysSinceLastSeen": 7
},
"sourceVatNumber": "NL999999999B99"
}

Change-event record (entity-verification mismatch — wire-transfer hold trigger):

{
"recordType": "change-event",
"eventType": "vat.name_mismatch",
"vatNumber": "FR12345678901",
"countryCode": "FR",
"severity": "high",
"timestamp": "2026-05-01T10:22:20.000Z",
"context": {
"viesName": "ACME LOGISTICS SARL",
"similarityScore": 38
},
"sourceVatNumber": "FR12345678901"
}

Output fields

FieldTypeDescription
recordTypeStringvalidation (success or VIES-said-invalid) / error (input or skipped) / fatal (actor-side fatal). Branch automation on this.
vatNumberStringFull VAT number as processed
countryCodeStringTwo-letter EU country code
numberStringVAT number without country prefix
validBooleanWhether VIES considers it currently registered
nameString or nullRegistered company name
addressString or nullRegistered business address
traderName / traderStreet / traderPostalCode / traderCity / traderCompanyTypeString or nullParsed trader fields when VIES provides them
requestDateStringISO 8601 VIES timestamp
requestIdentifierString or nullEU VIES consultation reference number (audit evidence — verified consultation mode only)
errorString or nullStable error code if validation failed
failureTypeString or nullStable enum: invalid-input / no-data-temp / no-data-permanent / rate-limited / network-error / vies-error
recommendationString or nullPlain-English next step for failure rows
dataCompletenessNumber or nullFraction of optional success fields populated (0.0-1.0)
missingFieldsArrayOptional fields that came back null
countryReliabilityString or nullhigh / medium / low based on documented VIES reliability per country
changeFlagsArrayCross-run change enum: NEW_VAT / VALIDITY_LOST / VALIDITY_GAINED / NAME_CHANGED / ADDRESS_CHANGED / UNCHANGED (only when compareToPrevRun is set)
changeSinceLastRunObject or nullDiff block: previousValid, previousName, previousAddress, firstSeenAt, lastSeenAt, daysSinceLastSeen
cacheHitBoolean or nullTrue when row served from cacheTTLHours cache (NOT charged)
cachedAgeHoursInteger or nullAge of the cached result in hours (only set on cache hits)
nameMatchBoolean or nullEntity verification: true when Levenshtein similarity ≥ 90
nameMatchScoreNumber or nullLevenshtein similarity 0-100 against expectedName
countryMatchBoolean or nullEntity verification: true when countryCode === expectedCountry
matchFlagsArrayEntity verification enum: NAME_MATCHED / NAME_PARTIAL_MATCH / NAME_MISMATCHED / COUNTRY_MATCHED / COUNTRY_MISMATCHED / NO_EXPECTED_DATA
severityString or nullSeverity tier: critical (VALIDITY_LOST), high (NAME_MISMATCHED, COUNTRY_MISMATCHED, currently invalid, VALIDITY_GAINED), medium (NAME_CHANGED, ADDRESS_CHANGED, NAME_PARTIAL_MATCH, completeness <30%), low (NEW_VAT, completeness <60%, transient infra failure), null (uneventful)
riskScoreNumber or nullComposite trust score 0.0-1.0 — combines validity (1.0 if invalid), name mismatch (+0.5), partial match (+0.25), VALIDITY_LOST (+1.0), NAME_CHANGED (+0.15), ADDRESS_CHANGED (+0.05), low completeness (+0.05 to +0.15), country reliability (+0.02 to +0.05)
riskLevelString or nullCoarse risk band: critical (≥0.75), high (≥0.5), medium (≥0.25), low (<0.25)
groupString or nullDerived row-grouping for spreadsheet/dashboard filtering: changed, new, unchanged, invalid, failed
explanationArray of stringsPlain-English description of what's notable about this row. Empty array on uneventful rows.
retryEligibleBoolean or nullTrue when this failure could benefit from re-running (transient infrastructure issue)
riskFactorsArray of stringsStable enum codes (mirror of explanation[] in machine-readable form). Codes: validity_lost, currently_invalid, name_mismatch, country_mismatch, name_partial_match, name_changed, address_changed, very_low_completeness, low_completeness, unreliable_country, medium_reliability_country. Empty on low-risk rows.
primaryRiskFactorString or nullTop contributor from riskFactors[] (severity-ordered). Single-string convenience for low-code tools (Zapier, n8n, webhook routers) that filter on equality, not array-contains.
actionRequiredBoolean or nullTrue when row needs human or downstream action (riskLevel ≥ medium OR retryEligible). Single-column filter for spreadsheets and dashboards.
recommendedActionString or nullStable enum: block_invoice, hold_payment, request_certificate, review, retry_later, monitor_only, none. Branch automation on this.
actionPriorityString or nullimmediate, high, normal, low. Sortable for triage queues.
actionPlaybookArray of strings2–4 step playbook for the action — usable verbatim in tickets, Slack messages, runbooks.
businessImpactString or nullPlain-English board-level explanation per primary risk factor.
financialRiskEstimateObject or null{ amount, currency, vatRateUsed, note } — VAT-liability estimate when expectedInvoiceAmount supplied AND row recommends block_invoice / hold_payment. 21% conservative EU rate. Indicative only — not tax advice. Override with your accounting system's actual member-state VAT rate before filing or invoicing.
firstSeenAtStringWhen this VAT first appeared in any prior run for this monitor key.
daysSinceFirstSeenIntegerDays since firstSeenAt.
timesSeenIntegerTotal runs this VAT has appeared in.
timesAlertedIntegerTotal runs this VAT has been alerted in.
firstAlertedAtString or nullWhen the alert first fired. Sticky across runs while alerted; null when resolved.
daysOpenInteger or nullDays the alert has been open. Null when not alerted.
slaBreachedBoolean or nullTrue when daysOpen >= slaTargetDays.
slaTargetDaysInteger or nullEcho of input for downstream filtering.
escalationLevelInteger0 (not alerted), 1 (within SLA), 2 (SLA breached), 3 (severely overdue ≥2× SLA).
escalationReasonString or nullPlain-English explanation of the escalation level.
change-event records only
eventTypeStringStable enum: vat.validity_lost, vat.validity_gained, vat.name_changed, vat.address_changed, vat.name_mismatch, vat.country_mismatch
timestampStringISO 8601 timestamp when the event was emitted
contextObjectBefore/after values relevant to the event (previousValid, currentValid, previousName, currentName, daysSinceLastSeen, similarityScore, etc.)
sourceVatNumberStringThe VAT number the event was derived from — joinable back to the validation row by vatNumber

Run summary (key-value store: SUMMARY)

{
"totalInput": 250,
"duplicatesSkipped": 4,
"totalProcessed": 246,
"valid": 218,
"invalid": 12,
"errors": 16,
"cacheHits": 30,
"chargedCount": 216,
"chargeLimitReached": false,
"circuitBrokenCountries": [],
"requesterVatNumber": "NL123456789B01",
"runStartedAt": "2026-05-01T10:00:00.000Z",
"runCompletedAt": "2026-05-01T10:00:48.000Z",
"mode": "auto",
"resolvedMode": "monitoring",
"byCountry": {
"FR": { "processed": 45, "valid": 42, "invalid": 1, "errors": 2 },
"NL": { "processed": 50, "valid": 48, "invalid": 2, "errors": 0 },
"IT": { "processed": 30, "valid": 28, "invalid": 1, "errors": 1 }
},
"errorBreakdown": {
"MS_UNAVAILABLE": 12,
"INVALID_COUNTRY_FORMAT": 4
},
"changeBreakdown": {
"NEW_VAT": 5,
"VALIDITY_LOST": 2,
"VALIDITY_GAINED": 0,
"NAME_CHANGED": 1,
"ADDRESS_CHANGED": 3,
"UNCHANGED": 215
},
"matchBreakdown": { "nameMatched": 0, "nameMismatched": 0, "nameNotChecked": 246, "countryMatched": 0, "countryMismatched": 0 },
"alerts": { "critical": 2, "high": 1, "medium": 0, "low": 13 },
"riskBreakdown": { "critical": 2, "high": 1, "medium": 0, "low": 243 },
"retryableCount": 12,
"hasChangeEvents": true,
"changeEventCount": 4,
"hasCriticalRisk": true,
"eventsSuppressedAsRepeats": 0,
"topRiskFactors": [
{ "factor": "validity_lost", "count": 2 },
{ "factor": "name_mismatch", "count": 1 },
{ "factor": "medium_reliability_country", "count": 73 }
],
"repeatOffenders": [
{ "vatNumber": "NL999999999B99", "timesSeen": 4, "timesAlerted": 4, "primaryRiskFactor": "validity_lost", "daysOpen": 21 }
],
"riskTrend": "increasing",
"slaBreaches": 1,
"escalationCounts": { "0": 233, "1": 11, "2": 1, "3": 1 }
}

Audit bundle (key-value store: AUDIT_BUNDLE)

Drop-in compliance archive — only meaningful when requesterVatNumber is set.

{
"schemaVersion": 1,
"runId": "abc123...",
"actorId": "ryanclinton/eu-vat-validator",
"datasetId": "def456...",
"requesterVatNumber": "NL123456789B01",
"runStartedAt": "2026-05-01T10:00:00.000Z",
"runCompletedAt": "2026-05-01T10:00:48.000Z",
"totalValidated": 246,
"totalValid": 218,
"consultationReferences": ["WAPIAAAAW7QwsB4n", "WAPIAAAAW7QwsBQy", "..."],
"auditEvidenceUri": "https://api.apify.com/v2/datasets/def456/items?view=audit&format=json",
"notes": "Verified consultation mode — every successful row carries a VIES consultation reference number that EU tax authorities accept as audit evidence under VAT Directive Art 138."
}

How much does it cost to validate EU VAT numbers?

EU VAT Number Validator uses pay-per-event pricing -- you pay $0.002 per VAT number validated. Platform compute costs are billed separately by Apify (typically a few cents per run).

You are NOT charged for:

  • Cache hits (cacheTTLHours reuse from prior run)
  • Skipped countries (DE without opt-in)
  • Failure rows (input invalid, country format invalid, VIES errors)
  • Numbers that hit a circuit breaker
ScenarioVAT NumbersTotal cost (worst case)
Quick test1$0.002
Small batch50$0.10
Medium batch200$0.40
Large batch1,000$2.00
Enterprise5,000$10.00
Maximum (per run)10,000$20.00

You can set a maximum spending limit per run to cap costs. The actor charges per result individually after pushData, so when the spending limit is reached the run stops cleanly mid-batch -- no leak of unbilled-but-pushed rows.

Compare this to commercial VAT validation APIs like Vatstack ($0.01-0.05/lookup), VATLayer ($14.99-99.99/month), or Vatlookup.eu ($0.02/query) -- this actor validates at $0.002/number with no monthly commitment AND adds change detection, entity verification, audit bundles, and per-country reliability hints none of them ship.

How EU VAT Number Validator compares

If you're choosing an EU VAT validation API: VIES is the source of truth, but this actor turns it into decision-ready, automation-friendly output — the only tool on Apify Store that combines audit-grade EU consultation references, cross-run change detection, entity verification with match scoring, SLA-based escalation, and portfolio-level repeat-offender tracking in one actor.

FeatureEU VAT Validator v3VatstackVATLayer
Compliance operations layer
Recommended action per row (block_invoice / hold_payment / review / retry_later / monitor_only)Yes — stable enum + 2–4 step playbookNoNo
SLA + escalation tracking (daysOpen, slaBreached, escalationLevel 0–3)Yes — sticky firstAlertedAt across runsNoNo
Business-impact plain-English per rowYes — board-level templatesNoNo
Financial risk estimate (when invoice amount supplied)Yes — indicative, not tax adviceNoNo
Repeat offenders (timesAlerted ≥ 3 + still alerted)Yes — top 10 in summaryNoNo
Risk trend vs prior run (increasing / decreasing / stable)YesNoNo
Triage view (sorted by escalation + days open)Yes — drop-in for finance ops queuesNoNo
Monitoring + audit
EU consultation reference (audit evidence)Yes — verified consultation modeNoNo
Cross-run change detectionYes — 6-flag enum + per-row diffNoNo
Change-event records (webhook-shaped)Yes — separate recordTypeNoNo
Repeat-event suppression (alert fatigue)Yes — emitOnlyNewEventsNoNo
Entity verification (match scoring)Yes — Levenshtein nameMatchScore + matchFlagsNoNo
Audit bundle KV record (drop-in archive)YesNoNo
Validation + intelligence
Data sourceOfficial VIES REST API (direct)VIES (via proxy)VIES (via proxy)
Trader fields parsed (5 separate fields)YesMerged onlyMerged only
Failure classification + recommendationYes — 6 stable enums + per-error next stepGeneric errorGeneric error
Data completeness scoring per rowYesNoNo
Country reliability hint per rowYesNoNo
Country format pre-validationYes — 28 country regex patternsNoNo
Input deduplicationYesNoNo
Cache TTL (skip recently-validated)Yes — and not chargedNoNo
Germany handlingOpt-in skip for unreliable DE nodeNoNo
Two-tier circuit breaker (global + per-country)YesNoNo
Throughput + UX
Bulk batch processingYes (up to 10,000)Yes (API)Yes (API)
Concurrent processingYes — 20 workers, per-country rate-limitedServer-sideServer-side
Retry on rate limitsYes (3x backoff on 429 + 5xx + network)Server-sideServer-side
Mode presetsYes — auto / quick / audit / monitoringNoNo
Pricing + delivery
Pricing modelPay per validation ($0.002)Per lookup ($0.01-0.05)Monthly subscription ($14.99+)
Free tier2,500 validations/month (Apify credits)100 lookups/month100 lookups/month
SchedulingBuilt-in (Apify Schedules)Not includedNot included
Output formatsJSON, CSV, Excel, Google SheetsJSONJSON, XML
Subscription requiredNoNoYes

In short:

  • Unlike Vatstack and VATLayer, this actor tracks VAT validity changes across runs (deregistrations, name changes, address changes) — they only return the current snapshot.
  • Unlike standard VAT APIs, the actor outputs decision-ready actions (block_invoice, hold_payment, review) instead of raw validity flags.
  • Unlike commercial VAT subscriptions, pricing is per-validation ($0.002) with no monthly commitment — and cache hits, skipped countries, and failures are not charged.
  • Unlike monolithic compliance tools, this actor is the deterministic SIGNAL generator; routing to Slack, Jira, Salesforce, or HubSpot happens via Apify Webhooks → your existing automation layer (more flexible, safer, single audit trail).
  • This is the only EU VAT tool on Apify Store that combines audit-grade EU consultation references, cross-run change detection, entity verification with match scoring, SLA-based escalation, and portfolio-level repeat-offender tracking in one actor.

Typical performance

MetricTypical value
VAT numbers per run1-10,000 (maxItems: 10000)
Run time (10 numbers)2-3 seconds
Run time (100 numbers)8-12 seconds
Run time (1,000 numbers)40-60 seconds
Run time (5,000 numbers)3-5 minutes
Concurrency20 global, per-country (FR=2, others=4)
Name/address return rate70-85% of valid numbers (varies by member state)
VIES uptime (most countries)95-99% during business hours
Germany (DE) VIES uptime40-60% (frequently offline)
Cost per run (typical)$0.01-$2.00 depending on batch size

Pay-per-use EU VAT validation API

Pay-per-use EU VAT validation API — no API key, no subscription, $0.002 per validation. JSON output, webhook-ready, no parsing required. Apify's free tier covers ~2,500 validations per month.

The actor wraps the official European Commission VIES REST API directly (no proxy). Authenticate with your Apify API token, POST your input, fetch results from the dataset endpoint. Use it as a simple "is this VAT valid?" call OR enable monitoring / audit / escalation as needed. Same actor, same endpoint.

Validate EU VAT numbers using the API

Pay-per-use EU VAT validation API — no API key, no subscription, $0.002 per validation. JSON output, webhook-ready, no parsing required.

Python — full compliance pipeline

from apify_client import ApifyClient
client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("ryanclinton/eu-vat-validator").call(run_input={
"vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"],
"mode": "monitoring",
"requesterVatNumber": "NL123456789B01",
"compareToPrevRun": True,
"monitorStateKey": "customer-db-q2",
"cacheTTLHours": 24,
"expectations": {
"FR40303265045": {"expectedName": "Total Energies SE"}
}
})
# Per-row results with change flags + match scores
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item.get("error"):
print(f"FAIL {item['vatNumber']}: {item['failureType']} -- {item['recommendation']}")
continue
flags = item.get("changeFlags", [])
match = item.get("matchFlags", [])
ref = item.get("requestIdentifier") or "no-ref"
print(f"{item['vatNumber']}: valid={item['valid']} flags={flags} match={match} ref={ref}")
# Run summary for dashboards
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("SUMMARY")
print(f"Changes: {summary['value']['changeBreakdown']}")
# Audit bundle for compliance archive
audit = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("AUDIT_BUNDLE")
if audit:
print(f"Archived {len(audit['value']['consultationReferences'])} VIES references for run {audit['value']['runId']}")

JavaScript

import { ApifyClient } from "apify-client";
const client = new ApifyClient({ token: "YOUR_API_TOKEN" });
const run = await client.actor("ryanclinton/eu-vat-validator").call({
vatNumbers: ["FR40303265045", "NL004495445B01", "IE6388047V"],
mode: "monitoring",
requesterVatNumber: "NL123456789B01",
compareToPrevRun: true,
monitorStateKey: "customer-db-q2",
cacheTTLHours: 24,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
if (item.error) {
console.log(`FAIL ${item.vatNumber}: ${item.failureType} -- ${item.recommendation}`);
continue;
}
const flags = item.changeFlags || [];
console.log(`${item.vatNumber}: valid=${item.valid} flags=${flags.join(",")} ref=${item.requestIdentifier || "no-ref"}`);
}
const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord("SUMMARY");
console.log("Changes:", summary.value.changeBreakdown);

cURL

curl -X POST "https://api.apify.com/v2/acts/ryanclinton~eu-vat-validator/runs?token=YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vatNumbers": ["FR40303265045", "NL004495445B01"],
"mode": "monitoring",
"requesterVatNumber": "NL123456789B01",
"compareToPrevRun": true,
"monitorStateKey": "customer-db-q2"
}'

For AI agents and automation systems

This actor is designed for programmatic decision-making — no free-text parsing required. Branch on stable enums, route by recommendedAction, escalate by escalationLevel.

In practice, this actor is the deterministic decision layer between raw VIES data and your automation stack. An AI agent calling this actor receives stable, machine-routable enums for every signal — no string parsing, no inference, no risk that the model interpreted "name mismatch" differently this time. The agent reads recommendedAction, branches on the enum value, uses actionPlaybook[] verbatim as the action body, and routes via actionPriority. This makes the actor a drop-in tool for compliance copilots, finance ops agents, and KYB pipelines that need predictable, auditable behavior across runs.

How an agent should use this actor:

  1. Call the actor with mode: "monitoring" and a stable monitorStateKey for ongoing surveillance, OR mode: "audit" with a requesterVatNumber for one-off compliance checks
  2. Filter dataset rows where actionRequired = true to find every counterparty that needs attention
  3. Branch on recommendedAction (stable enum) to route to the appropriate downstream tool: block_invoice → ERP freeze, hold_payment → AP queue, request_certificate → supplier email, retry_later → next scheduled run, review → human queue
  4. Use actionPlaybook[] verbatim as the body of the ticket / Slack message / email you generate
  5. Filter recordType = "change-event" rows to catch ONLY material changes since the last run — these are webhook-shaped for direct delivery to alerting channels
  6. For SLA enforcement, escalate when escalationLevel >= 2 (SLA breached) or escalationLevel = 3 (severely overdue ≥2× SLA)
  7. Pull the SUMMARY record from the default key-value store for portfolio-level signals (hasCriticalRisk, repeatOffenders, riskTrend, slaBreaches, escalationCounts)
  8. Pull the AUDIT_BUNDLE record for compliance archives — array of all VIES consultation reference numbers from the run, joinable to your tax-filing system

Stable enums an agent can branch on without parsing prose:

FieldValues
recordTypevalidation, error, fatal, change-event
recommendedActionblock_invoice, hold_payment, request_certificate, review, retry_later, monitor_only, none
actionPriorityimmediate, high, normal, low
failureTypeinvalid-input, no-data-temp, no-data-permanent, rate-limited, network-error, vies-error
riskLevelcritical, high, medium, low
severitycritical, high, medium, low, null
eventTypevat.validity_lost, vat.validity_gained, vat.name_changed, vat.address_changed, vat.first_seen, vat.name_mismatch, vat.country_mismatch
escalationLevel0 (not alerted), 1 (within SLA), 2 (SLA breached), 3 (severely overdue)
riskTrendincreasing, decreasing, stable, unknown
groupchanged, new, unchanged, invalid, failed
matchFlags[]NAME_MATCHED, NAME_PARTIAL_MATCH, NAME_MISMATCHED, COUNTRY_MATCHED, COUNTRY_MISMATCHED, NO_EXPECTED_DATA
changeFlags[]NEW_VAT, VALIDITY_LOST, VALIDITY_GAINED, NAME_CHANGED, ADDRESS_CHANGED, UNCHANGED
riskFactors[]validity_lost, currently_invalid, name_mismatch, country_mismatch, name_partial_match, name_changed, address_changed, very_low_completeness, low_completeness, unreliable_country, medium_reliability_country
countryReliabilityhigh, medium, low

All enums are stable across actor versions; new values may be added but existing values will not be renamed within a major version.

How EU VAT Number Validator works

Mode resolution

If mode: "auto" (default), the actor resolves to monitoring if compareToPrevRun is true, audit if requesterVatNumber is set, otherwise quick. The chosen mode applies preset defaults (e.g. monitoring enables compareToPrevRun, includeAddress, deduplicate, validateFormatLocally). Explicit user fields always win over preset defaults.

Input parsing, deduplication, and country format pre-check

Each raw VAT string is stripped of spaces, dots, and dashes, uppercased, and split into 2-letter country code + number. Duplicates (after normalisation) are removed by default. Each number is checked against a country-specific regex (28 patterns) -- typos return INVALID_COUNTRY_FORMAT without hitting VIES. Slow countries (FR, DE) are sorted last so fast countries return results first.

Cache lookup (monitoring mode + cacheTTLHours)

If compareToPrevRun is on AND cacheTTLHours is set, each VAT is checked against the prior state snapshot. If the entry exists and lastSeenAt is within the TTL, the row is served from cache (cacheHit: true) without hitting VIES and without being charged.

Concurrent VIES validation with retries

Validations run with 20 global concurrency. Per-country semaphores cap concurrent calls (FR at 2 because VIES throttles France aggressively, others at 4). Each VIES request has a 30-second timeout. On MS_MAX_CONCURRENT_REQ, SERVICE_UNAVAILABLE, HTTP 5xx, or network/timeout errors, the actor retries up to 3 times with increasing delays (3s, 6s, 9s).

When requesterVatNumber is set, every VIES call includes the requester's memberStateCode and number, which causes VIES to generate a consultation reference number and return it in the response's requestIdentifier field.

Change detection (cross-run diff)

If compareToPrevRun is on, each result is diffed against the prior snapshot keyed by normalised VAT. The changeFlags array reports NEW_VAT / VALIDITY_LOST / VALIDITY_GAINED / NAME_CHANGED / ADDRESS_CHANGED / UNCHANGED. The changeSinceLastRun block reports the prior valid / name / address and daysSinceLastSeen. State is persisted to a named KV store (eu-vat-validator-state) keyed by monitorStateKey.

Entity verification (match scoring)

If expectations is supplied, each row is matched: expectedName against VIES name via Levenshtein similarity (normalised: lowercase, common legal-form suffixes stripped, punctuation collapsed); expectedCountry against countryCode exact match. Flags (NAME_MATCHED / NAME_PARTIAL_MATCH / NAME_MISMATCHED) trip at score thresholds 90 / 70.

Two-tier circuit breaker

After every result, the actor tracks consecutive infrastructure failures (failureType: "no-data-temp", "rate-limited", or "network-error"). After 5 in a row in a single country, that country is marked broken — remaining numbers in that country fail-fast with COUNTRY_CIRCUIT_BROKEN (one bad country does not abort the run). After 10 consecutive failures globally, the global circuit breaker trips and the run stops.

Incremental flush + per-item charging

Results are pushed to the dataset in batches of 50. After each pushData, in pay-per-event mode, the actor charges $0.002 per item individually -- but NOT for cache hits or failure rows. So when the spending limit is reached, the run stops cleanly mid-batch.

Output

Per-row dataset records, plus SUMMARY (totals + by-country + change-flag breakdown + match breakdown + chargedCount + cacheHits + circuit-broken countries + resolved mode) and AUDIT_BUNDLE (drop-in compliance archive when requesterVatNumber is set) in the default key-value store.

Tips for best results

  1. Start with mode: "auto". It picks the right preset from your input. Switch to explicit modes only when you want to override.
  2. Use requesterVatNumber for audit evidence. Without it, VIES returns the validation result but no consultation reference number. The reference is the legal proof under VAT Directive Article 138.
  3. For scheduled monitoring, set a stable monitorStateKey. Use the same key across runs so change detection compares against the right snapshot. Auto-derived keys depend on input set; explicit keys are stable.
  4. In monitoring mode, set cacheTTLHours: 24 for daily schedules. VATs validated yesterday won't re-hit VIES today, and you won't be charged for them. Massive cost saving on stable customer databases.
  5. Use expectations on supplier onboarding. Pass the company name your supplier claims; the actor flags mismatches before the wire transfer.
  6. Filter the dataset by recordType and failureType. recordType: "validation" AND failureType: null for clean rows; failureType: "no-data-temp" for re-runnable failures.
  7. Use dataCompleteness for KYB pipelines. Filter dataCompleteness >= 0.8 to drop rows missing critical trader fields.
  8. Combine with entity verification across registries. Pair with OpenCorporates Search or GLEIF LEI Lookup to cross-reference VIES company names against independent corporate registries.
  9. Export directly to Google Sheets. Use the Apify Google Sheets integration to push validation results into a shared spreadsheet that finance or compliance can review.

Combine with other Apify actors

ActorHow to combine
OpenCorporates SearchCross-reference VIES company names + addresses against corporate registries in 140+ jurisdictions
UK Companies House SearchValidate UK business partners separately (GB VAT numbers are not in VIES post-Brexit)
GLEIF LEI LookupMatch validated VAT entities to their Legal Entity Identifiers for financial compliance
OpenSanctions SearchScreen validated companies against global sanctions, PEP, and watchlist databases
OFAC Sanctions SearchAdd US Treasury OFAC screening to your VAT validation pipeline for trade compliance
Australia ABN LookupValidate Australian Business Numbers for APAC trading partners alongside EU VAT checks
HubSpot Lead PusherPush validated company names and addresses directly into HubSpot CRM records

Limitations

  • VIES rate limits -- the European Commission enforces per-country rate limits. The actor mitigates with per-country concurrency caps + exponential backoff retries + two-tier circuit breaker, but very large concurrent runs may still encounter MS_MAX_CONCURRENT_REQ errors.
  • Germany (DE) node reliability -- DE's VIES node is frequently offline (40-60% uptime). Default skips DE; opt-in means accepting intermittent failures even after retries.
  • Incomplete name/address data -- not all member states return company details through VIES (~15-30% of valid numbers, varies by country). The dataCompleteness field flags these.
  • No historical data from VIES -- VIES only confirms current registration status. The actor's cross-run change detection (compareToPrevRun) reconstructs this over scheduled runs, but VIES itself has no historical API.
  • UK (GB) VAT numbers not supported -- post-Brexit. Only Northern Ireland (XI) is supported.
  • No country-specific check-digit verification -- the actor pre-validates against length/character patterns but does not run national checksum algorithms. VIES is the source of truth for validity.
  • VIES maintenance windows -- individual country nodes go offline during European evenings and weekends. The two-tier circuit breaker prevents budget burn; re-run during business hours.

Integrations

  • Zapier -- trigger VAT validation from a new CRM record or form submission, route on failureType + changeFlags for downstream tools
  • Make -- visual workflows for supplier onboarding or order processing
  • Google Sheets -- export validation results directly to a shared spreadsheet
  • Apify API -- embed validation into ERP systems, checkout flows, or compliance dashboards
  • Webhooks -- trigger downstream processing when a validation run completes
  • LangChain / LlamaIndex -- feed validated company data into AI-powered compliance analysis or entity resolution

Use in Dify

Drop this actor into Dify workflows via the Apify plugin's Run Actor node. Each VAT returns validated, scored, and recommended as structured JSON — block_invoice / hold_payment / request_certificate / review / retry_later / monitor_only / none plus actionPriority, actionPlaybook[], escalationLevel (0–3), and riskLevel your downstream node branches on. The VIES portal pointed at one VAT at a time returns "valid/invalid"; this returns decisions.

  • Actor ID: ryanclinton/eu-vat-validator
  • Sample input (verified-consultation supplier verification with audit reference):
{
"vatNumbers": ["FR40303265045", "NL004495445B01"],
"requesterVatNumber": "NL123456789B01",
"expectations": [
{ "vatNumber": "FR40303265045", "expectedName": "Total Energies SE", "expectedCountry": "FR" },
{ "vatNumber": "NL004495445B01", "expectedName": "Heineken NV" }
]
}
  • Branching example — a Dify if/else node reads recommendedAction and routes:
    • block_invoice → ERP-freeze tool
    • hold_payment → AP-queue tool
    • request_certificate → supplier-email tool
    • review → human-in-the-loop node
    • retry_later → scheduled rerun (next CET morning)
    • monitor_only / none → no-op exit
  • For monitoring workflows: set mode: "monitoring" + a stable monitorStateKey; the Dify workflow runs daily or weekly and surfaces only what changed (recordType: "change-event" rows with eventType in vat.validity_lost / vat.name_mismatch / etc.)
  • For audit pipelines: set requesterVatNumber and every row returns a VIES consultation reference (requestIdentifier) your Dify workflow can route to your tax-filing archive
  • For supplier onboarding chatbots: pass the supplier's claimed name in expectations and route on matchFlags @> ARRAY['NAME_MISMATCHED'] to escalate fraud-screening to a human

The actionPlaybook[] array is usable verbatim as the body of any Dify-generated ticket, Slack message, or email — no LLM rewriting required, fully deterministic across runs.

Troubleshooting

  • MS_UNAVAILABLE errors for a specific country -- VIES node temporarily offline. Filter failureType: "no-data-temp" and re-run during European business hours (08:00-17:00 CET). The recommendation field on each failed row says exactly this.
  • All German (DE) numbers show COUNTRY_UNRELIABLE_SKIPPED -- expected default behaviour. Set includeUnreliableCountries: true to attempt DE validation.
  • INVALID_COUNTRY_FORMAT for numbers that look correct -- the country regex caught a likely typo. If you're sure the number is legitimate, set validateFormatLocally: false to send it to VIES anyway.
  • COUNTRY_CIRCUIT_BROKEN -- the per-country circuit breaker tripped after 5 consecutive infrastructure failures for that country. Re-run later or split into a separate batch.
  • Run stops with "Stopped after VIES outage detected" -- the global circuit breaker tripped after 10 consecutive infrastructure failures. Re-run later. Filter the dataset by failureType in ['no-data-temp', 'rate-limited', 'network-error'] to identify which numbers still need validation.
  • Every row shows NEW_VAT even though I scheduled this run -- check that monitorStateKey matches the previous run. Auto-derived keys depend on input set; explicit keys are stable.
  • requestIdentifier is null on every row -- you ran without requesterVatNumber. VIES only returns consultation references when you supply your own VAT.
  • AUDIT_BUNDLE shows consultationReferences: [] -- same reason. Without requesterVatNumber, no references are generated.

Key takeaways

  • Compliance engine, not just a validator -- audit-grade output, cross-run change detection, entity verification, two-tier circuit breaker, and a drop-in audit bundle.
  • Official EU data source -- queries VIES directly. No proxy.
  • Audit-grade output (verified consultation mode) -- supply your own VAT and every result includes the EU consultation reference number tax authorities accept under VAT Directive Article 138.
  • Cross-run change detection -- compareToPrevRun produces NEW_VAT / VALIDITY_LOST / NAME_CHANGED / etc. flags + per-row diff. Turns one-shot tool into scheduled compliance monitor.
  • Entity verification -- expectations map produces match scoring against expected name + country.
  • Cost-efficient at scale -- $0.002 per validation; cache hits + failures + skipped countries NOT charged.
  • Fast batch processing -- 1,000 VAT numbers in about 50 seconds via 20-way concurrent processing with per-country rate limiting.
  • Stable failure classification + recommendations -- every failure row carries failureType enum + plain-English recommendation.
  • Smart Germany handling -- skips chronically unreliable DE VIES node by default.
  • Two-tier circuit breaker -- global breaker for VIES outages, per-country breaker for isolated bad nodes.

Responsible use

  • This actor queries publicly available business registration data from the European Commission's VIES REST API. It does not bypass authentication, CAPTCHAs, or access restricted content.
  • The VIES API is a free public service provided by the European Commission for legitimate business-to-business VAT verification. The actor enforces per-country rate limits to avoid overloading the service.
  • Users are responsible for ensuring their use of VAT validation results complies with applicable laws, including GDPR when storing company data linked to natural persons (e.g., sole traders).
  • Do not misrepresent VIES validation results as legal certification or tax advice. Consult a qualified tax professional for compliance decisions.
  • For guidance on web scraping legality, see Apify's guide.

FAQ

What's the best EU VAT validation API? The official source is the European Commission's VIES REST API. This actor wraps the official VIES API directly (no proxy) and adds the layers VIES alone does not provide: cross-run change detection, audit-grade EU consultation reference numbers, entity verification with match scoring, SLA-based escalation, and stable enums for downstream automation. If you're choosing an EU VAT validation API: VIES is the source of truth, but this actor turns it into decision-ready, automation-friendly output.

Is this just a VAT validator or something more? It's a VAT compliance monitoring and automation system, not just a validator. Most validators stop at "is this VAT valid right now?". This actor adds three more layers — Decision (stable recommendedAction enum + 2–4 step playbook), Escalation (SLA target + daysOpen + escalationLevel 0-3 across scheduled runs), and Audit (EU consultation reference numbers preserved on every row + drop-in AUDIT_BUNDLE archive). Designed specifically for ongoing VAT compliance monitoring across scheduled runs.

Is there a free EU VAT validation API I can use as a developer? The official EU VIES API itself is free, but it's a SOAP/REST endpoint with no client libraries, no rate-limit handling, and no bulk validation. This actor wraps the official VIES REST API with no API key required, no subscription, no monthly fee — pay-per-validation at $0.002 each. Apify's free tier includes $5/month of credits (~2,500 validations free per month). Drop-in for developers: JSON output, webhook-ready, no parsing required. Use it as a simple validity check OR turn on monitoring / audit / escalation as needed.

Is there a pay-per-use EU VAT validation API? Yes. Pay-per-use EU VAT validation API — no API key, no subscription, $0.002 per validation. JSON output, webhook-ready, no parsing required. Apify's free tier covers ~2,500 validations per month. Use it as a simple "is this VAT valid?" call OR enable monitoring, audit, and SLA escalation as needed. Same actor, same endpoint.

How do I automatically detect invalid VAT numbers across my customer database? Run the actor in mode: "monitoring" with a stable monitorStateKey and your customer VAT list as input. Schedule it daily, weekly, or monthly via Apify Schedules. The actor emits recordType: "change-event" records on each material change (validity loss, name change, etc.) — wire those to your Slack / Jira / Zapier flow via Apify Webhooks. Filter recommendedAction = "block_invoice" for the rows that need immediate finance-ops action.

How do I block invoices automatically when a VAT becomes invalid? Filter dataset rows where `recommendedActi