EU VAT Number Validator (VIES)
Pricing
from $2.00 / 1,000 vat validateds
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
Actor stats
0
Bookmarked
11
Total users
2
Monthly active users
2 days ago
Last modified
Categories
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 EUreference 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:
| Decision | Filter on | Where it appears |
|---|---|---|
| Block a zero-rated invoice if buyer VAT is invalid | recommendedAction = "block_invoice" | Every validation row |
| Hold a payment when supplier identity does not match | recommendedAction = "hold_payment" | Every validation row |
| Open a Jira ticket with priority + playbook | actionPriority = "immediate" + use actionPlaybook[] as ticket steps | Every validation row |
| Estimate VAT exposure in your accounting feed | financialRiskEstimate.amount (set expectedInvoiceAmount per expectation) | Every validation row when supplied |
| Escalate stale issues to the finance lead | escalationLevel >= 2 OR slaBreached = true | Every validation row + summary escalationCounts |
| Identify repeat offenders for vendor review | summary.repeatOffenders[] (timesAlerted ≥ 3 AND currently alerted) | KV SUMMARY record |
| Track compliance drift over time | summary.riskTrend in ("increasing", "decreasing", "stable") | KV SUMMARY record |
| Page on-call when a customer is suddenly deregistered | recordType = "change-event" AND eventType = "vat.validity_lost" | Change-event records (set emitChangeEvents: true) |
| Hold a wire transfer when supplier identity does not match VIES | eventType = "vat.name_mismatch" AND severity = "high" | Change-event records (requires expectations) |
| Filter Sheets to "needs action" rows only | actionRequired = TRUE | Every validation row |
| Route by reason in SQL/agent tool calls | riskFactors @> 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 hours | retryEligible = true | Failures view |
| Suppress alert spam on persistent issues | emitOnlyNewEvents: true (auto-on in monitoring mode) | Input |
| Send a quarterly compliance archive to auditors | AUDIT_BUNDLE key-value record | Storage tab |
| Trigger a critical-risk alert | summary.hasCriticalRisk = true | KV SUMMARY record |
| Track monitor health | summary.alerts.{critical,high,medium,low} counts | KV 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
modepreset). 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 = 7slaBreached = false slaBreached = false slaBreached = TRUEescalationLevel = 1 escalationLevel = 1 escalationLevel = 2recommendedAction = 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 alertsyour finance lead via thesummary.escalationCounts."2"counter→ repeatOffenders[] insummary 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 = nullriskTrend = "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:
| Destination | How |
|---|---|
| Slack | Apify Webhook → Zapier/Make/n8n → Slack incoming webhook. Filter severity = "high" OR recommendedAction = "block_invoice". |
| Microsoft Teams | Same pattern — Teams accepts incoming webhooks identically. Filter on escalationLevel >= 2 for SLA-breach alerts. |
| Jira / Linear / GitHub Issues | Zapier or n8n "Create issue" step. Use actionPlaybook[] as the body, actionPriority (immediate/high/normal/low) as Priority, recommendedAction as a label. |
| PagerDuty | Filter 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 / Zapier | Branch on recommendedAction for routing; pass actionPlaybook[] as the action template. |
| Salesforce / HubSpot CRM | Update the supplier record with riskLevel + escalationLevel; create a task when actionRequired = true. |
| Webhooks generally | Any 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.
| Layer | Capability |
|---|---|
| Action engine ⭐ | recommendedAction 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 + escalation ⭐ | slaTargetDays input; per-row firstAlertedAt (sticky), daysOpen, slaBreached, escalationLevel (0–3), escalationReason; summary.slaBreaches + summary.escalationCounts for ops queues |
| Business impact ⭐ | businessImpact plain-English board-level explanation per primary risk factor; optional financialRiskEstimate (21% conservative EU VAT rate) when expectedInvoiceAmount supplied per expectation |
| Lifecycle metadata | Read-only resolution signals — firstSeenAt, daysSinceFirstSeen, timesSeen, timesAlerted per row; summary.repeatOffenders (top 10 VATs alerted in 3+ runs and still alerted) |
| Portfolio intelligence | summary.topRiskFactors (top 5 by count), summary.repeatOffenders, summary.riskTrend (increasing / decreasing / stable / unknown — vs prior run's alert count) |
| Triage view | New dataset view sorting alerted rows by escalation level + days open — drop-in for finance ops triage queues |
What's in v2 (foundation)
| Layer | Capability |
|---|---|
| Monitoring | Cross-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 layer | Optional 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 |
| Audit | Verified consultation mode → EU consultation reference numbers per row; AUDIT_BUNDLE KV record drop-in for tax archives; audit mode preset |
| Verification | Per-VAT expectations with expectedName / expectedCountry → match scoring (Levenshtein), nameMatch / nameMatchScore / countryMatch / matchFlags enum |
| Risk + decision layer | riskScore (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 |
| Intelligence | Per-row dataCompleteness (0..1) + missingFields[], countryReliability hint, failureType enum + plain-English recommendation, retryEligible boolean |
| Validation | Direct VIES REST, 28-country format pre-check, per-country rate-limited concurrency, retries with backoff, two-tier circuit breaker (global + per-country) |
| UX | mode preset (auto / quick / audit / monitoring) — set the mode, get sensible defaults; explicit fields always win; expectations accepts array OR object form |
| Output | recordType 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:
| Signal | Recommended action | Routing destination |
|---|---|---|
recommendedAction = "block_invoice" | Stop pending zero-rated invoice | Finance ops Slack channel + accounting freeze |
recommendedAction = "hold_payment" | Pause in-flight payment | AP queue + supplier notification |
recommendedAction = "request_certificate" | Email supplier for updated VAT cert | Supplier outreach automation |
recommendedAction = "retry_later" | Re-run during business hours | Apify Schedules (next CET morning) |
escalationLevel >= 2 | Escalate to finance lead | PagerDuty + email + Slack mention |
escalationLevel = 3 | Severely overdue — escalate to compliance director | PagerDuty critical incident |
summary.hasCriticalRisk = true | Trigger compliance review meeting | Calendar block + dashboard alert |
summary.repeatOffenders[] not empty | Add to vendor-review report | Quarterly procurement review queue |
eventType = "vat.validity_lost" | Alert account owner immediately | CRM update + sales-rep notification |
eventType = "vat.name_mismatch" | Pause + investigate fraud signal | Compliance ticket + account-owner review |
recordType = "change-event" AND severity = "high" | Slack alert with actionPlaybook as body | #compliance-alerts |
actionRequired = TRUE | Spreadsheet 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 invalid | recommendedAction = "block_invoice" (or valid = false + archive requestIdentifier for audit evidence) |
| Supplier's company name doesn't match what VIES says | eventType = "vat.name_mismatch" (set expectations first to enable scoring) |
| Customer was valid last quarter but is now deregistered | changeFlags @> ARRAY['VALIDITY_LOST'] (or eventType = "vat.validity_lost") |
| Country VIES node went down mid-run | failureType = "no-data-temp" AND retryEligible = true |
| Need legal proof you verified the buyer before issuing the invoice | Set 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 issues | slaTargetDays: 7 input + filter escalationLevel >= 2 |
| Don't want to spam Slack on the same persistent issue every run | Set emitOnlyNewEvents: true (auto-on in monitoring mode) |
| Run this daily but only changes matter | mode: "monitoring" + cacheTTLHours: 24 + emitOnlyChanges: true (no-cost no-noise loop) |
| Need to flag VATs that have been problematic across many runs | summary.repeatOffenders[] (timesAlerted ≥ 3 AND currently alerted) |
| Track whether compliance is getting better or worse | summary.riskTrend (increasing / decreasing / stable / unknown) |
| Fraud-screening: catch suppliers operating under different legal entity | eventType = "vat.country_mismatch" |
| Estimate VAT liability exposure on a high-risk row | Provide expectedInvoiceAmount per expectation; row gets financialRiskEstimate.amount (indicative — not tax advice) |
What data can you extract with EU VAT validation?
| Data Point | Source | Example |
|---|---|---|
| VAT Number | Input (cleaned) | FR40303265045 |
| Country Code | Parsed from input | FR |
| Validity Status | VIES API response | true |
| Company Name | VIES member state data | TOTAL ENERGIES SE |
| Registered Address | VIES member state data | 2 PLACE JEAN MILLIER, LA DEFENSE 6, 92400 COURBEVOIE |
| Trader Name / Street / Postal Code / City / Company Type | VIES parsed fields | TOTAL ENERGIES SE / 2 PLACE JEAN MILLIER / 92400 / COURBEVOIE / SE |
| Request Date | VIES API timestamp | 2026-05-01T10:22:16.000+01:00 |
| VIES Consultation Reference (audit) | VIES requestIdentifier | WAPIAAAAW7QwsB4n |
| 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 similarity | 94 |
| Match Flags (verification mode) | Entity verification | ["NAME_MATCHED", "COUNTRY_MATCHED"] |
| Data Completeness | Computed | 0.71 (5 of 7 optional fields populated) |
| Country Reliability | Static hint | low (DE), medium (FR/IT/ES), high (others) |
| Failure Type (classified) | Actor logic | no-data-temp, invalid-input, rate-limited |
| Recommendation (next step) | Actor logic | Re-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:
- Speed at scale -- 20-way concurrency with per-country rate limiting validates 1,000 numbers in about 50 seconds.
- 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 anAUDIT_BUNDLErecord in the key-value store as a drop-in archive for tax filings. - Stateful change detection -- enable
compareToPrevRunand 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. - Entity verification -- supply
expectationswith 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. - Compliance-ready failure handling -- every failure carries a stable
failureTypeenum and a plain-Englishrecommendation, 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
compareToPrevRunto get the diff for free - Cache TTL -- in monitoring mode, opt into
cacheTTLHoursand 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 — requiresrequesterVatNumber), ormonitoring(cross-run change detection on by default). Explicit fields always override preset defaults. - Verified consultation mode (audit-grade) -- supply your own VAT in
requesterVatNumberand VIES returns arequestIdentifierfor 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
compareToPrevRunto compare results against the previous run's state (stored in named KV storeeu-vat-validator-state, keyed bymonitorStateKey). Every row carries achangeFlagsarray (NEW_VAT,VALIDITY_LOST,VALIDITY_GAINED,NAME_CHANGED,ADDRESS_CHANGED,UNCHANGED) and achangeSinceLastRunblock (previous values +daysSinceLastSeen+firstSeenAt+lastSeenAt). State is FIFO-capped at 50,000 entries. First run for a key isNEW_VATon every row. - Cache TTL (cost saver in monitoring mode) -- set
cacheTTLHourstogether withcompareToPrevRunand VATs validated within that window are returned from state instead of hitting VIES. Cache hits are flagged withcacheHit: trueand are NOT charged. - Entity verification -- supply
expectations(map of VAT →{ expectedName, expectedCountry }) and every row getsnameMatch(true if Levenshtein similarity ≥ 90),nameMatchScore(0-100),countryMatch, and amatchFlagsenum (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, andtraderCompanyTypeas 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 withINVALID_COUNTRY_FORMATbefore they reach VIES. Saves money and reduces VIES load. Disable withvalidateFormatLocally: 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 withdataCompleteness >= 0.8). - Input deduplication -- duplicate VAT numbers (case-insensitive, normalised) are removed before processing. Default on; disable with
deduplicate: falseif 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. EnableincludeUnreliableCountriesto 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, andAbortErrortimeouts. - 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
failureTypeenum (invalid-input/no-data-temp/no-data-permanent/rate-limited/network-error/vies-error) and a plain-Englishrecommendation. recordTypediscriminator -- every row carriesrecordType: '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 withrequesterVatNumber, anAUDIT_BUNDLErecord 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
- 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. - Pick a mode --
auto(default) is the right pick for most users. Useauditif you need consultation reference numbers (and supplyrequesterVatNumber). Usemonitoringfor scheduled compliance checks (turns oncompareToPrevRunand pairs well with a stablemonitorStateKey). - (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
requestIdentifieryou can archive as proof. - (Optional) Add expectations -- For entity verification, supply
expectationsas{ "FR40303265045": { "expectedName": "Total Energies SE" } }. Every row will include a match score. - Run the actor -- Click "Start" and wait. 100 numbers complete in about 10 seconds. 1,000 numbers in about 50 seconds.
- 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.
- Read the run summary + audit bundle -- The Storage > Key-value store tab has a
SUMMARYrecord with country breakdown, error histogram, change-flag counts, totals. If you ran in verified mode, anAUDIT_BUNDLErecord is also there as a drop-in compliance archive.
Input parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
vatNumbers | Array of strings | Yes | ["NL004495445B01"] | List of EU VAT numbers to validate (max 10,000). Country prefix required. Spaces, dots, dashes stripped. |
mode | Select | No | auto | Workflow preset: auto / quick / audit / monitoring. Explicit fields always win over preset defaults. |
requesterVatNumber | String | No | -- | Your own EU VAT. When set, VIES returns a consultation reference number (audit evidence under VAT Directive Art 138). |
compareToPrevRun | Boolean | No | false (auto-on in monitoring mode) | Compare against the previous run's state. Emits changeFlags + changeSinceLastRun per row. |
monitorStateKey | String | No | auto-derived | Stable identifier for the change-detection state snapshot. Reuse across scheduled runs. |
emitChangeEvents | Boolean | No | false (auto-on in monitoring mode) | Emit additional recordType: 'change-event' rows for every material change. Webhook-shaped, NOT charged. |
emitOnlyChanges | Boolean | No | false | Suppress uneventful validation rows in monitoring mode. VIES calls still happen and bill normally — pair with cacheTTLHours for true no-cost monitoring. |
emitOnlyNewEvents | Boolean | No | false (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. |
cacheTTLHours | Integer (1-720) | No | -- | When set with compareToPrevRun, skip VIES re-validation for VATs checked within this many hours. Cache hits are NOT charged. |
slaTargetDays | Integer (1-365) | No | 7 | SLA target in days. Drives daysOpen / slaBreached / escalationLevel / escalationReason per row. Only meaningful with compareToPrevRun. |
expectations | Array OR Object | No | -- | 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. |
deduplicate | Boolean | No | true | Skip duplicate VAT numbers (matched after normalisation). |
validateFormatLocally | Boolean | No | true | Pre-validate against country regex; numbers that fail return INVALID_COUNTRY_FORMAT without hitting VIES. |
includeUnreliableCountries | Boolean | No | false | Attempt validation for Germany (DE), whose VIES node is frequently offline. |
includeAddress | Boolean | No | true | Include 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, not129273398. - Use
ELfor Greece -- the VIES system usesEL(Ellada), notGR. - Use
XIfor 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
cacheTTLHoursto control VIES load + cost -- in monitoring mode with daily schedules, setcacheTTLHours: 24to 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
includeUnreliableCountriesoff 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
| Field | Type | Description |
|---|---|---|
recordType | String | validation (success or VIES-said-invalid) / error (input or skipped) / fatal (actor-side fatal). Branch automation on this. |
vatNumber | String | Full VAT number as processed |
countryCode | String | Two-letter EU country code |
number | String | VAT number without country prefix |
valid | Boolean | Whether VIES considers it currently registered |
name | String or null | Registered company name |
address | String or null | Registered business address |
traderName / traderStreet / traderPostalCode / traderCity / traderCompanyType | String or null | Parsed trader fields when VIES provides them |
requestDate | String | ISO 8601 VIES timestamp |
requestIdentifier | String or null | EU VIES consultation reference number (audit evidence — verified consultation mode only) |
error | String or null | Stable error code if validation failed |
failureType | String or null | Stable enum: invalid-input / no-data-temp / no-data-permanent / rate-limited / network-error / vies-error |
recommendation | String or null | Plain-English next step for failure rows |
dataCompleteness | Number or null | Fraction of optional success fields populated (0.0-1.0) |
missingFields | Array | Optional fields that came back null |
countryReliability | String or null | high / medium / low based on documented VIES reliability per country |
changeFlags | Array | Cross-run change enum: NEW_VAT / VALIDITY_LOST / VALIDITY_GAINED / NAME_CHANGED / ADDRESS_CHANGED / UNCHANGED (only when compareToPrevRun is set) |
changeSinceLastRun | Object or null | Diff block: previousValid, previousName, previousAddress, firstSeenAt, lastSeenAt, daysSinceLastSeen |
cacheHit | Boolean or null | True when row served from cacheTTLHours cache (NOT charged) |
cachedAgeHours | Integer or null | Age of the cached result in hours (only set on cache hits) |
nameMatch | Boolean or null | Entity verification: true when Levenshtein similarity ≥ 90 |
nameMatchScore | Number or null | Levenshtein similarity 0-100 against expectedName |
countryMatch | Boolean or null | Entity verification: true when countryCode === expectedCountry |
matchFlags | Array | Entity verification enum: NAME_MATCHED / NAME_PARTIAL_MATCH / NAME_MISMATCHED / COUNTRY_MATCHED / COUNTRY_MISMATCHED / NO_EXPECTED_DATA |
severity | String or null | Severity 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) |
riskScore | Number or null | Composite 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) |
riskLevel | String or null | Coarse risk band: critical (≥0.75), high (≥0.5), medium (≥0.25), low (<0.25) |
group | String or null | Derived row-grouping for spreadsheet/dashboard filtering: changed, new, unchanged, invalid, failed |
explanation | Array of strings | Plain-English description of what's notable about this row. Empty array on uneventful rows. |
retryEligible | Boolean or null | True when this failure could benefit from re-running (transient infrastructure issue) |
riskFactors | Array of strings | Stable 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. |
primaryRiskFactor | String or null | Top contributor from riskFactors[] (severity-ordered). Single-string convenience for low-code tools (Zapier, n8n, webhook routers) that filter on equality, not array-contains. |
actionRequired | Boolean or null | True when row needs human or downstream action (riskLevel ≥ medium OR retryEligible). Single-column filter for spreadsheets and dashboards. |
recommendedAction | String or null | Stable enum: block_invoice, hold_payment, request_certificate, review, retry_later, monitor_only, none. Branch automation on this. |
actionPriority | String or null | immediate, high, normal, low. Sortable for triage queues. |
actionPlaybook | Array of strings | 2–4 step playbook for the action — usable verbatim in tickets, Slack messages, runbooks. |
businessImpact | String or null | Plain-English board-level explanation per primary risk factor. |
financialRiskEstimate | Object 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. |
firstSeenAt | String | When this VAT first appeared in any prior run for this monitor key. |
daysSinceFirstSeen | Integer | Days since firstSeenAt. |
timesSeen | Integer | Total runs this VAT has appeared in. |
timesAlerted | Integer | Total runs this VAT has been alerted in. |
firstAlertedAt | String or null | When the alert first fired. Sticky across runs while alerted; null when resolved. |
daysOpen | Integer or null | Days the alert has been open. Null when not alerted. |
slaBreached | Boolean or null | True when daysOpen >= slaTargetDays. |
slaTargetDays | Integer or null | Echo of input for downstream filtering. |
escalationLevel | Integer | 0 (not alerted), 1 (within SLA), 2 (SLA breached), 3 (severely overdue ≥2× SLA). |
escalationReason | String or null | Plain-English explanation of the escalation level. |
| change-event records only | ||
eventType | String | Stable enum: vat.validity_lost, vat.validity_gained, vat.name_changed, vat.address_changed, vat.name_mismatch, vat.country_mismatch |
timestamp | String | ISO 8601 timestamp when the event was emitted |
context | Object | Before/after values relevant to the event (previousValid, currentValid, previousName, currentName, daysSinceLastSeen, similarityScore, etc.) |
sourceVatNumber | String | The 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 (
cacheTTLHoursreuse from prior run) - Skipped countries (DE without opt-in)
- Failure rows (input invalid, country format invalid, VIES errors)
- Numbers that hit a circuit breaker
| Scenario | VAT Numbers | Total cost (worst case) |
|---|---|---|
| Quick test | 1 | $0.002 |
| Small batch | 50 | $0.10 |
| Medium batch | 200 | $0.40 |
| Large batch | 1,000 | $2.00 |
| Enterprise | 5,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.
| Feature | EU VAT Validator v3 | Vatstack | VATLayer |
|---|---|---|---|
| Compliance operations layer | |||
| Recommended action per row (block_invoice / hold_payment / review / retry_later / monitor_only) | Yes — stable enum + 2–4 step playbook | No | No |
| SLA + escalation tracking (daysOpen, slaBreached, escalationLevel 0–3) | Yes — sticky firstAlertedAt across runs | No | No |
| Business-impact plain-English per row | Yes — board-level templates | No | No |
| Financial risk estimate (when invoice amount supplied) | Yes — indicative, not tax advice | No | No |
| Repeat offenders (timesAlerted ≥ 3 + still alerted) | Yes — top 10 in summary | No | No |
| Risk trend vs prior run (increasing / decreasing / stable) | Yes | No | No |
| Triage view (sorted by escalation + days open) | Yes — drop-in for finance ops queues | No | No |
| Monitoring + audit | |||
| EU consultation reference (audit evidence) | Yes — verified consultation mode | No | No |
| Cross-run change detection | Yes — 6-flag enum + per-row diff | No | No |
| Change-event records (webhook-shaped) | Yes — separate recordType | No | No |
| Repeat-event suppression (alert fatigue) | Yes — emitOnlyNewEvents | No | No |
| Entity verification (match scoring) | Yes — Levenshtein nameMatchScore + matchFlags | No | No |
| Audit bundle KV record (drop-in archive) | Yes | No | No |
| Validation + intelligence | |||
| Data source | Official VIES REST API (direct) | VIES (via proxy) | VIES (via proxy) |
| Trader fields parsed (5 separate fields) | Yes | Merged only | Merged only |
| Failure classification + recommendation | Yes — 6 stable enums + per-error next step | Generic error | Generic error |
| Data completeness scoring per row | Yes | No | No |
| Country reliability hint per row | Yes | No | No |
| Country format pre-validation | Yes — 28 country regex patterns | No | No |
| Input deduplication | Yes | No | No |
| Cache TTL (skip recently-validated) | Yes — and not charged | No | No |
| Germany handling | Opt-in skip for unreliable DE node | No | No |
| Two-tier circuit breaker (global + per-country) | Yes | No | No |
| Throughput + UX | |||
| Bulk batch processing | Yes (up to 10,000) | Yes (API) | Yes (API) |
| Concurrent processing | Yes — 20 workers, per-country rate-limited | Server-side | Server-side |
| Retry on rate limits | Yes (3x backoff on 429 + 5xx + network) | Server-side | Server-side |
| Mode presets | Yes — auto / quick / audit / monitoring | No | No |
| Pricing + delivery | |||
| Pricing model | Pay per validation ($0.002) | Per lookup ($0.01-0.05) | Monthly subscription ($14.99+) |
| Free tier | 2,500 validations/month (Apify credits) | 100 lookups/month | 100 lookups/month |
| Scheduling | Built-in (Apify Schedules) | Not included | Not included |
| Output formats | JSON, CSV, Excel, Google Sheets | JSON | JSON, XML |
| Subscription required | No | No | Yes |
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
| Metric | Typical value |
|---|---|
| VAT numbers per run | 1-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 |
| Concurrency | 20 global, per-country (FR=2, others=4) |
| Name/address return rate | 70-85% of valid numbers (varies by member state) |
| VIES uptime (most countries) | 95-99% during business hours |
| Germany (DE) VIES uptime | 40-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 ApifyClientclient = 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 scoresfor item in client.dataset(run["defaultDatasetId"]).iterate_items():if item.get("error"):print(f"FAIL {item['vatNumber']}: {item['failureType']} -- {item['recommendation']}")continueflags = 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 dashboardssummary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("SUMMARY")print(f"Changes: {summary['value']['changeBreakdown']}")# Audit bundle for compliance archiveaudit = 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:
- Call the actor with
mode: "monitoring"and a stablemonitorStateKeyfor ongoing surveillance, ORmode: "audit"with arequesterVatNumberfor one-off compliance checks - Filter dataset rows where
actionRequired = trueto find every counterparty that needs attention - 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 - Use
actionPlaybook[]verbatim as the body of the ticket / Slack message / email you generate - Filter
recordType = "change-event"rows to catch ONLY material changes since the last run — these are webhook-shaped for direct delivery to alerting channels - For SLA enforcement, escalate when
escalationLevel >= 2(SLA breached) orescalationLevel = 3(severely overdue ≥2× SLA) - Pull the
SUMMARYrecord from the default key-value store for portfolio-level signals (hasCriticalRisk,repeatOffenders,riskTrend,slaBreaches,escalationCounts) - Pull the
AUDIT_BUNDLErecord 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:
| Field | Values |
|---|---|
recordType | validation, error, fatal, change-event |
recommendedAction | block_invoice, hold_payment, request_certificate, review, retry_later, monitor_only, none |
actionPriority | immediate, high, normal, low |
failureType | invalid-input, no-data-temp, no-data-permanent, rate-limited, network-error, vies-error |
riskLevel | critical, high, medium, low |
severity | critical, high, medium, low, null |
eventType | vat.validity_lost, vat.validity_gained, vat.name_changed, vat.address_changed, vat.first_seen, vat.name_mismatch, vat.country_mismatch |
escalationLevel | 0 (not alerted), 1 (within SLA), 2 (SLA breached), 3 (severely overdue) |
riskTrend | increasing, decreasing, stable, unknown |
group | changed, 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 |
countryReliability | high, 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
- Start with
mode: "auto". It picks the right preset from your input. Switch to explicit modes only when you want to override. - Use
requesterVatNumberfor 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. - 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. - In monitoring mode, set
cacheTTLHours: 24for 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. - Use
expectationson supplier onboarding. Pass the company name your supplier claims; the actor flags mismatches before the wire transfer. - Filter the dataset by
recordTypeandfailureType.recordType: "validation"ANDfailureType: nullfor clean rows;failureType: "no-data-temp"for re-runnable failures. - Use
dataCompletenessfor KYB pipelines. FilterdataCompleteness >= 0.8to drop rows missing critical trader fields. - Combine with entity verification across registries. Pair with OpenCorporates Search or GLEIF LEI Lookup to cross-reference VIES company names against independent corporate registries.
- 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
| Actor | How to combine |
|---|---|
| OpenCorporates Search | Cross-reference VIES company names + addresses against corporate registries in 140+ jurisdictions |
| UK Companies House Search | Validate UK business partners separately (GB VAT numbers are not in VIES post-Brexit) |
| GLEIF LEI Lookup | Match validated VAT entities to their Legal Entity Identifiers for financial compliance |
| OpenSanctions Search | Screen validated companies against global sanctions, PEP, and watchlist databases |
| OFAC Sanctions Search | Add US Treasury OFAC screening to your VAT validation pipeline for trade compliance |
| Australia ABN Lookup | Validate Australian Business Numbers for APAC trading partners alongside EU VAT checks |
| HubSpot Lead Pusher | Push 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_REQerrors. - 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
dataCompletenessfield 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+changeFlagsfor 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
recommendedActionand routes:block_invoice→ ERP-freeze toolhold_payment→ AP-queue toolrequest_certificate→ supplier-email toolreview→ human-in-the-loop noderetry_later→ scheduled rerun (next CET morning)monitor_only/none→ no-op exit
- For monitoring workflows: set
mode: "monitoring"+ a stablemonitorStateKey; the Dify workflow runs daily or weekly and surfaces only what changed (recordType: "change-event"rows witheventTypeinvat.validity_lost/vat.name_mismatch/ etc.) - For audit pipelines: set
requesterVatNumberand 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
expectationsand route onmatchFlags @> 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_UNAVAILABLEerrors for a specific country -- VIES node temporarily offline. FilterfailureType: "no-data-temp"and re-run during European business hours (08:00-17:00 CET). Therecommendationfield on each failed row says exactly this.- All German (DE) numbers show
COUNTRY_UNRELIABLE_SKIPPED-- expected default behaviour. SetincludeUnreliableCountries: trueto attempt DE validation. INVALID_COUNTRY_FORMATfor numbers that look correct -- the country regex caught a likely typo. If you're sure the number is legitimate, setvalidateFormatLocally: falseto 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_VATeven though I scheduled this run -- check thatmonitorStateKeymatches the previous run. Auto-derived keys depend on input set; explicit keys are stable. requestIdentifieris null on every row -- you ran withoutrequesterVatNumber. VIES only returns consultation references when you supply your own VAT.AUDIT_BUNDLEshowsconsultationReferences: []-- same reason. WithoutrequesterVatNumber, 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 --
compareToPrevRunproducesNEW_VAT/VALIDITY_LOST/NAME_CHANGED/ etc. flags + per-row diff. Turns one-shot tool into scheduled compliance monitor. - Entity verification --
expectationsmap 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
failureTypeenum + plain-Englishrecommendation. - 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