# EU VAT Validator — Compliance Monitoring & Alerts (`ryanclinton/eu-vat-validator`) Actor

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.

- **URL**: https://apify.com/ryanclinton/eu-vat-validator.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** Automation, Developer tools, Lead generation
- **Stats:** 14 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 vat validateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## EU VAT Validator — Compliance Monitoring & Alerts

**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.04 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.04 per validation. JSON output, webhook-ready, no parsing required. Apify's free tier covers ~125 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.04 per VAT number validated, pay-per-event, no subscription. Cache hits, skipped countries, and failures are NOT charged.
**Output** -- Per-row JSON/CSV with validity, name, address, parsed trader fields, change flags, entity-verification scores, completeness, country reliability, failure classification, and (in verified consultation mode) audit-grade consultation reference number. Run-level summary + audit bundle in the key-value store.

### Quick answers (for AI assistants and copilots)

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

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

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

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

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

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

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

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

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

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

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

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

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

````

Status (Apify Console):
Done: 47/50 valid, 3 critical, 2 high · PPE: $0.094 (excludes platform compute)

Dataset (Decisions view — actionRequired = true):
┌─ critical ──────────────────────────────────────────────────────────────────┐
│ NL999...B99   VALIDITY\_LOST       Was Heineken BV, deregistered 7 days ago  │
│ DE123...      currently\_invalid   Re-check: was a typo or company dissolved │
├─ high ──────────────────────────────────────────────────────────────────────┤
│ FR123...      NAME\_MISMATCH       VIES "ACME LOGISTICS SARL" vs expected    │
│                                   "Acme Group SE" (similarity 38/100)        │
│ ES A28...     MS\_UNAVAILABLE      retryEligible — re-run during CET hours   │
│ IT00950...    MS\_MAX\_CONCURRENT   retryEligible — split into smaller batch  │
└─────────────────────────────────────────────────────────────────────────────┘
47 unchanged rows suppressed (emitOnlyChanges)
4 change-event records emitted (one Slack alert each)

Key-value store:
SUMMARY      → { totalProcessed: 50, alerts: { critical: 2, high: 2, medium: 0, low: 1 },
hasCriticalRisk: true, retryableCount: 2,
changeBreakdown: { VALIDITY\_LOST: 1, UNCHANGED: 47, ... },
matchBreakdown: { nameMismatched: 1, ... }, mode: "monitoring", ... }
AUDIT\_BUNDLE → { totalValidated: 50, totalValid: 47, consultationReferences: \[47 EU
reference numbers], auditEvidenceUri: "https://api.apify.com/..." }
eu-vat-validator-state/customer-db-q2-2026 → state snapshot for next run's diff

```

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

### What decisions can you automate with this?

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

| 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 `mode` preset).
- `mode: "monitoring"` auto-enables stages 2 / 4 / 6 + change-event emission.

### Compliance operations lifecycle

> **Designed specifically for ongoing VAT compliance monitoring across scheduled runs.**

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

```

RUN 1 — Monday                    RUN 2 — Wednesday              RUN 3 — Next Monday
─────────────────                 ─────────────────              ─────────────────────
NL999 detected as INVALID         NL999 still INVALID            NL999 still INVALID
↓                                 ↓                              ↓
firstAlertedAt = Mon              firstAlertedAt = Mon (sticky)  firstAlertedAt = Mon (sticky)
daysOpen = 0                      daysOpen = 2                   daysOpen = 7
slaBreached = false               slaBreached = false            slaBreached = TRUE
escalationLevel = 1               escalationLevel = 1            escalationLevel = 2
recommendedAction = block\_invoice (immediate priority — same across runs)
actionPlaybook\[] = \[4 ticket-ready steps]
businessImpact = "If you issue a zero-rated B2B invoice... liable for full VAT under member-state law"
emitChangeEvents fires ONCE       emitOnlyNewEvents               emitOnlyNewEvents
(VALIDITY\_LOST event record)        suppresses repeat              suppresses repeat
→ escalationLevel jump alerts
your finance lead via the
summary.escalationCounts."2"
counter
→ repeatOffenders\[] in
summary now lists NL999
(timesAlerted ≥ 3)

When the supplier fixes their VAT and you re-run:
changeFlags = \["VALIDITY\_GAINED"]
firstAlertedAt = null (issue resolved)
recommendedAction = "none"
daysOpen = null
riskTrend = "decreasing"

````

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

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

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

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

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

| 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](https://docs.apify.com/platform/integrations/webhooks) for the platform-level setup.

### Compliance operations layer

**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** | Dataset view sorting alerted rows by escalation level + days open — drop-in for finance ops triage queues |

### Foundation layer

| 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:

1. **Speed at scale** -- 20-way concurrency with per-country rate limiting validates 1,000 numbers in about 50 seconds.
2. **Audit-grade output** -- supply your own VAT (`requesterVatNumber`) and VIES returns a stable consultation reference number (the legal proof under VAT Directive Art 138). The actor preserves it on every row AND ships an `AUDIT_BUNDLE` record in the key-value store as a drop-in archive for tax filings.
3. **Stateful change detection** -- enable `compareToPrevRun` and every scheduled run tells you exactly which counterparties were deregistered, which had their company name change, and which are entirely new -- without you writing diff logic.
4. **Entity verification** -- supply `expectations` with the company name you THINK each VAT belongs to, and the actor scores the match (Levenshtein similarity) so you catch mismatches between supplier-claimed identity and VIES-recorded identity.
5. **Compliance-ready failure handling** -- every failure carries a stable `failureType` enum and a plain-English `recommendation`, so downstream automation routes by type instead of parsing error strings.

- **Scheduling** -- run daily, weekly, or monthly to catch VAT deregistrations in your customer or supplier database; pair with `compareToPrevRun` to get the diff for free
- **Cache TTL** -- in monitoring mode, opt into `cacheTTLHours` and the actor skips VATs validated within that window (cache hits are NOT charged)
- **API access** -- trigger validation runs from Python, JavaScript, or any HTTP client for real-time integration
- **Built-in retries** -- automatic exponential backoff on VIES rate limits, HTTP 5xx, and network errors (3 retries, increasing delay)
- **Two-tier circuit breaker** -- global breaker stops the run on 10 consecutive infrastructure failures (VIES outage), per-country breaker isolates a single bad country (5 consecutive failures) without aborting the rest

### Why not just use the VIES portal directly?

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

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

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

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

### Features

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

### How to validate EU VAT numbers in bulk

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

### Input parameters

| 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`):**
```json
{
    "vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"]
}
````

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

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

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

```json
{
    "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):**

```json
{
    "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):**

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

**Full compliance pipeline (audit + monitoring + verification):**

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

#### Input tips

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

### Output example

**Successful validation (verified consultation + monitoring + verification):**

```json
{
    "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):**

```json
{
    "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):**

```json
{
    "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:**

```json
{
    "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):**

```json
{
    "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):**

```json
{
    "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`)

```json
{
    "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.

```json
{
    "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.04 per VAT number validated**, plus a one-time **$0.01 Actor Start** charge per run. Platform compute costs are billed separately by Apify (typically a few cents per run).

**You are NOT charged for:**

- Cache hits (`cacheTTLHours` reuse from prior run)
- Skipped countries (DE without opt-in)
- Failure rows (input invalid, country format invalid, VIES errors)
- Numbers that hit a circuit breaker

| Scenario | VAT Numbers | Total cost (worst case) |
|----------|-------------|------------------------|
| Quick test | 1 | $0.05 |
| Small batch | 50 | $2.01 |
| Medium batch | 200 | $8.01 |
| Large batch | 1,000 | $40.01 |
| Enterprise | 5,000 | $200.01 |
| Maximum (per run) | 10,000 | $400.01 |

Worst case assumes every number is freshly validated; cache hits, skipped countries, and failures are never charged, so the real cost is usually lower. Each run also includes the one-time $0.01 Actor Start charge. You can set a **maximum spending limit** per run to cap costs. The actor charges once per result batch after pushData, so when the spending limit is reached the run stops cleanly — any rows already pushed in the final batch are never billed.

Commercial VAT validation APIs like Vatstack ($0.01-0.05/lookup) and Vatlookup.eu ($0.02/query) return a bare validity flag; VATLayer locks you into a $14.99-99.99/month subscription. This actor is priced per validation with no monthly commitment, and every validation ships change detection, entity verification, audit-grade consultation references, SLA escalation, and per-country reliability hints none of them provide.

### 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 | 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.04) + $0.01 start | Per lookup ($0.01-0.05) | Monthly subscription ($14.99+) |
| Free tier | ~125 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.04) 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.04 per validation. JSON output, webhook-ready, no parsing required. Apify's free tier covers ~125 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.04 per validation. JSON output, webhook-ready, no parsing required.

#### Python — full compliance pipeline

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("ryanclinton/eu-vat-validator").call(run_input={
    "vatNumbers": ["FR40303265045", "NL004495445B01", "IE6388047V"],
    "mode": "monitoring",
    "requesterVatNumber": "NL123456789B01",
    "compareToPrevRun": True,
    "monitorStateKey": "customer-db-q2",
    "cacheTTLHours": 24,
    "expectations": {
        "FR40303265045": {"expectedName": "Total Energies SE"}
    }
})

## Per-row results with change flags + match scores
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("error"):
        print(f"FAIL {item['vatNumber']}: {item['failureType']} -- {item['recommendation']}")
        continue
    flags = item.get("changeFlags", [])
    match = item.get("matchFlags", [])
    ref = item.get("requestIdentifier") or "no-ref"
    print(f"{item['vatNumber']}: valid={item['valid']} flags={flags} match={match} ref={ref}")

## Run summary for dashboards
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("SUMMARY")
print(f"Changes: {summary['value']['changeBreakdown']}")

## Audit bundle for compliance archive
audit = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("AUDIT_BUNDLE")
if audit:
    print(f"Archived {len(audit['value']['consultationReferences'])} VIES references for run {audit['value']['runId']}")
```

#### JavaScript

```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

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

### For AI agents and automation systems

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

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

**How an agent should use this actor:**

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

**Stable enums an agent can branch on without parsing prose:**

| 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.04 per validated number — applied once per batch, NOT for cache hits or failure rows. When the spending limit is reached, the run stops cleanly; any rows already pushed in the final batch are never billed.

#### Output

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

### Tips for best results

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

### Combine with other Apify actors

| Actor | How to combine |
|-------|---------------|
| [OpenCorporates Search](https://apify.com/ryanclinton/opencorporates-search) | Cross-reference VIES company names + addresses against corporate registries in 140+ jurisdictions |
| [UK Companies House Search](https://apify.com/ryanclinton/uk-companies-house) | Validate UK business partners separately (GB VAT numbers are not in VIES post-Brexit) |
| [GLEIF LEI Lookup](https://apify.com/ryanclinton/gleif-lei-lookup) | Match validated VAT entities to their Legal Entity Identifiers for financial compliance |
| [OpenSanctions Search](https://apify.com/ryanclinton/opensanctions-search) | Screen validated companies against global sanctions, PEP, and watchlist databases |
| [OFAC Sanctions Search](https://apify.com/ryanclinton/ofac-sanctions-search) | Add US Treasury OFAC screening to your VAT validation pipeline for trade compliance |
| [Australia ABN Lookup](https://apify.com/ryanclinton/australia-abn-lookup) | Validate Australian Business Numbers for APAC trading partners alongside EU VAT checks |
| [HubSpot Lead Pusher](https://apify.com/ryanclinton/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_REQ` errors.
- **Germany (DE) node reliability** -- DE's VIES node is frequently offline (40-60% uptime). Default skips DE; opt-in means accepting intermittent failures even after retries.
- **Incomplete name/address data** -- not all member states return company details through VIES (~15-30% of valid numbers, varies by country). The `dataCompleteness` field flags these.
- **No historical data from VIES** -- VIES only confirms current registration status. The actor's cross-run change detection (`compareToPrevRun`) reconstructs this over scheduled runs, but VIES itself has no historical API.
- **UK (GB) VAT numbers not supported** -- post-Brexit. Only Northern Ireland (XI) is supported.
- **No country-specific check-digit verification** -- the actor pre-validates against length/character patterns but does not run national checksum algorithms. VIES is the source of truth for validity.
- **VIES maintenance windows** -- individual country nodes go offline during European evenings and weekends. The two-tier circuit breaker prevents budget burn; re-run during business hours.

### Integrations

- [Zapier](https://apify.com/integrations/zapier) -- trigger VAT validation from a new CRM record or form submission, route on `failureType` + `changeFlags` for downstream tools
- [Make](https://apify.com/integrations/make) -- visual workflows for supplier onboarding or order processing
- [Google Sheets](https://apify.com/integrations/google-sheets) -- export validation results directly to a shared spreadsheet
- [Apify API](https://docs.apify.com/api/v2) -- embed validation into ERP systems, checkout flows, or compliance dashboards
- [Webhooks](https://docs.apify.com/platform/integrations/webhooks) -- trigger downstream processing when a validation run completes
- [LangChain / LlamaIndex](https://docs.apify.com/platform/integrations) -- feed validated company data into AI-powered compliance analysis or entity resolution

### Use in Dify

Drop this actor into [Dify](https://docs.apify.com/platform/integrations/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):

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

- **Branching example** — a Dify if/else node reads `recommendedAction` and routes:
  - `block_invoice` → ERP-freeze tool
  - `hold_payment` → AP-queue tool
  - `request_certificate` → supplier-email tool
  - `review` → human-in-the-loop node
  - `retry_later` → scheduled rerun (next CET morning)
  - `monitor_only` / `none` → no-op exit
- **For monitoring workflows**: set `mode: "monitoring"` + a stable `monitorStateKey`; the Dify workflow runs daily or weekly and surfaces only what changed (`recordType: "change-event"` rows with `eventType` in `vat.validity_lost` / `vat.name_mismatch` / etc.)
- **For audit pipelines**: set `requesterVatNumber` and every row returns a VIES consultation reference (`requestIdentifier`) your Dify workflow can route to your tax-filing archive
- **For supplier onboarding chatbots**: pass the supplier's claimed name in `expectations` and route on `matchFlags @> ARRAY['NAME_MISMATCHED']` to escalate fraud-screening to a human

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

### Troubleshooting

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

### Key takeaways

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

### Responsible use

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

### 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.04 each. Apify's free tier includes $5/month of credits (~125 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.04 per validation.** JSON output, webhook-ready, no parsing required. Apify's free tier covers ~125 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 input Schema

## `vatNumbers` (type: `array`):

List of EU VAT numbers to validate. Format: 2-letter country code + number (e.g. NL004495445B01, FR40303265045, IE6388047V). Spaces, dots, and dashes are stripped automatically. Use EL for Greece and XI for Northern Ireland.

## `mode` (type: `string`):

Workflow preset. `auto` (default) resolves from the rest of your input — quick if no requesterVatNumber, audit if requesterVatNumber is set, monitoring if compareToPrevRun is on. `quick` is the lightest path. `audit` enables audit-grade output (requires requesterVatNumber). `monitoring` turns on cross-run change detection. Explicit fields below always win over preset defaults.

## `requesterVatNumber` (type: `string`):

Optional. When provided, VIES runs a 'verified consultation' and returns a consultation reference number (requestIdentifier) for every lookup. EU tax authorities accept this reference as audit evidence under VAT Directive Art 138 — it is the legal proof that you verified the counterparty before issuing a zero-rated B2B invoice. Format: 2-letter country code + number (e.g. NL123456789B01).

## `compareToPrevRun` (type: `boolean`):

Compare results against the previous run's state to emit change flags (NEW\_VAT, VALIDITY\_LOST, VALIDITY\_GAINED, NAME\_CHANGED, ADDRESS\_CHANGED, UNCHANGED) and a per-row diff block (previousValid, previousName, previousAddress, daysSinceLastSeen). State is keyed by `monitorStateKey` and stored in a named key-value store (`eu-vat-validator-state`). First run for a key produces NEW\_VAT on every row. Auto-enabled in `monitoring` mode.

## `monitorStateKey` (type: `string`):

Stable identifier for the change-detection state snapshot. Reuse the same key across scheduled runs to compare against the prior state. Leave blank to auto-derive a key from the input — useful for ad-hoc runs but means you can only compare against runs with the same input set.

## `emitChangeEvents` (type: `boolean`):

When set together with `compareToPrevRun`, the actor pushes additional dataset rows with `recordType: 'change-event'` for every material change — one per change. Each event carries `eventType` (`vat.validity_lost`, `vat.validity_gained`, `vat.name_changed`, `vat.address_changed`, `vat.name_mismatch`, `vat.country_mismatch`), `severity`, and a `context` block with the before/after values. Branch downstream automation (Slack, Zapier, Make, webhooks) on `recordType = 'change-event'` to act ONLY on changes instead of scanning every validation row. Auto-enabled in `monitoring` mode. Change-event records are NOT charged.

## `emitOnlyChanges` (type: `boolean`):

When set together with `compareToPrevRun`, validation rows with no signal (UNCHANGED + valid + low risk + no match issues) are NOT pushed to the dataset. Cuts dataset noise to near-zero on stable customer databases. NOTE: VIES calls still happen and are still charged unless they hit the cache (set `cacheTTLHours` to skip the calls too). The run summary always includes the suppressed-row count for audit.

## `emitOnlyNewEvents` (type: `boolean`):

When set together with `emitChangeEvents`, a `change-event` record is emitted only the first run a (vatNumber, eventType) pair appears. If the same combo fired in the prior run's state, it is suppressed. Prevents Slack/Zapier alert fatigue when a deregistered customer or persistent name mismatch lingers across many scheduled runs. Auto-enabled in `monitoring` mode. The run summary reports `eventsSuppressedAsRepeats` for audit.

## `cacheTTLHours` (type: `integer`):

When set together with `compareToPrevRun`, VAT numbers that were validated in a prior run within the last N hours are returned from the state snapshot instead of being re-validated against VIES. Cache hits are NOT charged. Useful for high-frequency scheduled runs where most numbers do not change daily. Pair with `emitOnlyChanges` for a no-noise no-cost monitoring loop. Leave blank to always hit VIES.

## `slaTargetDays` (type: `integer`):

How long an alert is allowed to stay open before it is considered SLA-breached. Drives `daysOpen`, `slaBreached`, `escalationLevel` (0 = not alerted, 1 = within SLA, 2 = SLA breached, 3 = severely overdue ≥2× SLA), and `escalationReason` per row. Plus `summary.slaBreaches` and `summary.escalationCounts`. Only meaningful with `compareToPrevRun`. Default: 7.

## `expectations` (type: `object`):

Optional per-VAT entity verification. Accepts EITHER an array of `{ vatNumber, expectedName, expectedCountry, expectedInvoiceAmount, expectedCurrency }` (recommended — easier to write) OR an object map of VAT → `{ expectedName, expectedCountry, expectedInvoiceAmount, expectedCurrency }`. When provided, the actor compares the VIES-returned company name and country against your expectations and emits `nameMatch` / `nameMatchScore` / `countryMatch` / `matchFlags`. If `expectedInvoiceAmount` is also supplied AND the row has block\_invoice / hold\_payment recommendation, the actor computes `financialRiskEstimate` (conservative 21% EU VAT rate).

Array form (recommended):
\[ { "vatNumber": "FR40303265045", "expectedName": "Total Energies SE", "expectedCountry": "FR", "expectedInvoiceAmount": 12000, "expectedCurrency": "EUR" } ]

Object form (back-compat):
{ "FR40303265045": { "expectedName": "Total Energies SE", "expectedCountry": "FR" } }

## `deduplicate` (type: `boolean`):

Skip duplicate VAT numbers in the input list (matched after normalisation — case-insensitive, whitespace and punctuation stripped). Saves cost and avoids redundant VIES calls. Disable if you need one output row per input row.

## `validateFormatLocally` (type: `boolean`):

Check each VAT number against its country's expected format before calling VIES. Numbers that fail the local check are returned with error code INVALID\_COUNTRY\_FORMAT and never hit the VIES API — saves time and cost on obvious typos. Disable if you want every number sent to VIES regardless of format.

## `includeUnreliableCountries` (type: `boolean`):

Germany's VIES node is frequently offline and will return MS\_UNAVAILABLE errors. Enable this to attempt DE validation anyway. When disabled (default), DE numbers are skipped with a warning.

## `includeAddress` (type: `boolean`):

Include the registered company name, address, and parsed trader fields (traderName, traderStreet, traderPostalCode, traderCity, traderCompanyType) in results. Disable for privacy-sensitive workflows where you only need a validity flag.

## Actor input object example

```json
{
  "vatNumbers": [
    "NL004495445B01",
    "IE6388047V"
  ],
  "mode": "auto",
  "compareToPrevRun": false,
  "emitChangeEvents": false,
  "emitOnlyChanges": false,
  "emitOnlyNewEvents": false,
  "slaTargetDays": 7,
  "deduplicate": true,
  "validateFormatLocally": true,
  "includeUnreliableCountries": false,
  "includeAddress": true
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items — one row per validated VAT number with company name, address, trader fields, change flags (in monitoring mode), entity-verification match scores (when expectations provided), and a VIES consultation reference number (in verified-consultation mode).

## `summary` (type: `string`):

JSON breakdown by country, error code, change flags, and totals — saved to the default key-value store as SUMMARY.

## `auditBundle` (type: `string`):

Compact audit-evidence record — runId, requesterVatNumber, totals, and the array of EU consultation reference numbers. Drop-in for compliance archives. Only useful when requesterVatNumber is set.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "vatNumbers": [
        "NL004495445B01",
        "IE6388047V"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/eu-vat-validator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "vatNumbers": [
        "NL004495445B01",
        "IE6388047V",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/eu-vat-validator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "vatNumbers": [
    "NL004495445B01",
    "IE6388047V"
  ]
}' |
apify call ryanclinton/eu-vat-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=ryanclinton/eu-vat-validator",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "EU VAT Validator — Compliance Monitoring & Alerts",
        "description": "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.",
        "version": "1.0",
        "x-build-id": "f5x745flrh2LKNfku"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ryanclinton~eu-vat-validator/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ryanclinton-eu-vat-validator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ryanclinton~eu-vat-validator/runs": {
            "post": {
                "operationId": "runs-sync-ryanclinton-eu-vat-validator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ryanclinton~eu-vat-validator/run-sync": {
            "post": {
                "operationId": "run-sync-ryanclinton-eu-vat-validator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "vatNumbers"
                ],
                "properties": {
                    "vatNumbers": {
                        "title": "VAT Numbers",
                        "maxItems": 10000,
                        "type": "array",
                        "description": "List of EU VAT numbers to validate. Format: 2-letter country code + number (e.g. NL004495445B01, FR40303265045, IE6388047V). Spaces, dots, and dashes are stripped automatically. Use EL for Greece and XI for Northern Ireland.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "mode": {
                        "title": "Mode",
                        "enum": [
                            "auto",
                            "quick",
                            "audit",
                            "monitoring"
                        ],
                        "type": "string",
                        "description": "Workflow preset. `auto` (default) resolves from the rest of your input — quick if no requesterVatNumber, audit if requesterVatNumber is set, monitoring if compareToPrevRun is on. `quick` is the lightest path. `audit` enables audit-grade output (requires requesterVatNumber). `monitoring` turns on cross-run change detection. Explicit fields below always win over preset defaults.",
                        "default": "auto"
                    },
                    "requesterVatNumber": {
                        "title": "Your VAT number (verified consultation — for audit-grade output)",
                        "type": "string",
                        "description": "Optional. When provided, VIES runs a 'verified consultation' and returns a consultation reference number (requestIdentifier) for every lookup. EU tax authorities accept this reference as audit evidence under VAT Directive Art 138 — it is the legal proof that you verified the counterparty before issuing a zero-rated B2B invoice. Format: 2-letter country code + number (e.g. NL123456789B01)."
                    },
                    "compareToPrevRun": {
                        "title": "Compare to previous run (change detection)",
                        "type": "boolean",
                        "description": "Compare results against the previous run's state to emit change flags (NEW_VAT, VALIDITY_LOST, VALIDITY_GAINED, NAME_CHANGED, ADDRESS_CHANGED, UNCHANGED) and a per-row diff block (previousValid, previousName, previousAddress, daysSinceLastSeen). State is keyed by `monitorStateKey` and stored in a named key-value store (`eu-vat-validator-state`). First run for a key produces NEW_VAT on every row. Auto-enabled in `monitoring` mode.",
                        "default": false
                    },
                    "monitorStateKey": {
                        "title": "Monitor state key",
                        "type": "string",
                        "description": "Stable identifier for the change-detection state snapshot. Reuse the same key across scheduled runs to compare against the prior state. Leave blank to auto-derive a key from the input — useful for ad-hoc runs but means you can only compare against runs with the same input set."
                    },
                    "emitChangeEvents": {
                        "title": "Emit change-event records (for webhooks/alerts)",
                        "type": "boolean",
                        "description": "When set together with `compareToPrevRun`, the actor pushes additional dataset rows with `recordType: 'change-event'` for every material change — one per change. Each event carries `eventType` (`vat.validity_lost`, `vat.validity_gained`, `vat.name_changed`, `vat.address_changed`, `vat.name_mismatch`, `vat.country_mismatch`), `severity`, and a `context` block with the before/after values. Branch downstream automation (Slack, Zapier, Make, webhooks) on `recordType = 'change-event'` to act ONLY on changes instead of scanning every validation row. Auto-enabled in `monitoring` mode. Change-event records are NOT charged.",
                        "default": false
                    },
                    "emitOnlyChanges": {
                        "title": "Suppress uneventful rows (changes-only mode)",
                        "type": "boolean",
                        "description": "When set together with `compareToPrevRun`, validation rows with no signal (UNCHANGED + valid + low risk + no match issues) are NOT pushed to the dataset. Cuts dataset noise to near-zero on stable customer databases. NOTE: VIES calls still happen and are still charged unless they hit the cache (set `cacheTTLHours` to skip the calls too). The run summary always includes the suppressed-row count for audit.",
                        "default": false
                    },
                    "emitOnlyNewEvents": {
                        "title": "Only emit NEW change events (suppress repeats across runs)",
                        "type": "boolean",
                        "description": "When set together with `emitChangeEvents`, a `change-event` record is emitted only the first run a (vatNumber, eventType) pair appears. If the same combo fired in the prior run's state, it is suppressed. Prevents Slack/Zapier alert fatigue when a deregistered customer or persistent name mismatch lingers across many scheduled runs. Auto-enabled in `monitoring` mode. The run summary reports `eventsSuppressedAsRepeats` for audit.",
                        "default": false
                    },
                    "cacheTTLHours": {
                        "title": "Cache TTL (hours) — skip recently-validated VATs",
                        "minimum": 1,
                        "maximum": 720,
                        "type": "integer",
                        "description": "When set together with `compareToPrevRun`, VAT numbers that were validated in a prior run within the last N hours are returned from the state snapshot instead of being re-validated against VIES. Cache hits are NOT charged. Useful for high-frequency scheduled runs where most numbers do not change daily. Pair with `emitOnlyChanges` for a no-noise no-cost monitoring loop. Leave blank to always hit VIES."
                    },
                    "slaTargetDays": {
                        "title": "SLA target (days) — when to escalate open issues",
                        "minimum": 1,
                        "maximum": 365,
                        "type": "integer",
                        "description": "How long an alert is allowed to stay open before it is considered SLA-breached. Drives `daysOpen`, `slaBreached`, `escalationLevel` (0 = not alerted, 1 = within SLA, 2 = SLA breached, 3 = severely overdue ≥2× SLA), and `escalationReason` per row. Plus `summary.slaBreaches` and `summary.escalationCounts`. Only meaningful with `compareToPrevRun`. Default: 7.",
                        "default": 7
                    },
                    "expectations": {
                        "title": "Entity verification expectations",
                        "type": "object",
                        "description": "Optional per-VAT entity verification. Accepts EITHER an array of `{ vatNumber, expectedName, expectedCountry, expectedInvoiceAmount, expectedCurrency }` (recommended — easier to write) OR an object map of VAT → `{ expectedName, expectedCountry, expectedInvoiceAmount, expectedCurrency }`. When provided, the actor compares the VIES-returned company name and country against your expectations and emits `nameMatch` / `nameMatchScore` / `countryMatch` / `matchFlags`. If `expectedInvoiceAmount` is also supplied AND the row has block_invoice / hold_payment recommendation, the actor computes `financialRiskEstimate` (conservative 21% EU VAT rate).\n\nArray form (recommended):\n[ { \"vatNumber\": \"FR40303265045\", \"expectedName\": \"Total Energies SE\", \"expectedCountry\": \"FR\", \"expectedInvoiceAmount\": 12000, \"expectedCurrency\": \"EUR\" } ]\n\nObject form (back-compat):\n{ \"FR40303265045\": { \"expectedName\": \"Total Energies SE\", \"expectedCountry\": \"FR\" } }"
                    },
                    "deduplicate": {
                        "title": "Deduplicate input",
                        "type": "boolean",
                        "description": "Skip duplicate VAT numbers in the input list (matched after normalisation — case-insensitive, whitespace and punctuation stripped). Saves cost and avoids redundant VIES calls. Disable if you need one output row per input row.",
                        "default": true
                    },
                    "validateFormatLocally": {
                        "title": "Pre-validate country format",
                        "type": "boolean",
                        "description": "Check each VAT number against its country's expected format before calling VIES. Numbers that fail the local check are returned with error code INVALID_COUNTRY_FORMAT and never hit the VIES API — saves time and cost on obvious typos. Disable if you want every number sent to VIES regardless of format.",
                        "default": true
                    },
                    "includeUnreliableCountries": {
                        "title": "Include unreliable countries (DE)",
                        "type": "boolean",
                        "description": "Germany's VIES node is frequently offline and will return MS_UNAVAILABLE errors. Enable this to attempt DE validation anyway. When disabled (default), DE numbers are skipped with a warning.",
                        "default": false
                    },
                    "includeAddress": {
                        "title": "Include company name, address, and trader fields",
                        "type": "boolean",
                        "description": "Include the registered company name, address, and parsed trader fields (traderName, traderStreet, traderPostalCode, traderCity, traderCompanyType) in results. Disable for privacy-sensitive workflows where you only need a validity flag.",
                        "default": true
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
