API di validazione P.IVA UE

Cosa fa questa API

validate_vat verifica una partita IVA UE in due passi. Prima normalizza l'input (spazi, punti e trattini vengono ignorati, GR diventa EL) e controlla in locale formato e checksum nazionali, per i 27 Stati membri e XI (Irlanda del Nord). Poi interroga in tempo reale il VIES, il servizio della Commissione europea, tramite la sua API REST ufficiale. Denominazione e indirizzo registrati nel VIES non vengono restituiti (condizioni d'uso del VIES): invia i tuoi name e/o address per ottenere i booleani name_match / address_match, calcolati in locale.

Controllo automatico: regole locali di formato e checksum più una consultazione live del VIES (Commissione europea). Non è un'attestazione fiscale né una consulenza legale; i dati VIES provengono dalle banche dati degli Stati membri. Conserva una tua traccia della consultazione VIES.

Stati

L'API non restituisce mai invalid perché il VIES è giù.

Cache degli esiti VIES

Il VIES considera abuso verificare lo stesso numero più volte nello stesso giorno, quindi gli esiti vanno in cache: valid/invalid per 24 h, unverified/unverified_ms_down per 10 min (checks.vies.retry_after_s = tra quanto il VIES verrà interrogato di nuovo). La cache è condivisa da tutte le istanze del server (Firestore vies_cache, con chiave un HMAC del numero, mai il numero) e le richieste concorrenti per lo stesso numero condividono una sola chiamata VIES. checked_at è l'ora della verifica VIES originale e cache_age_s la sua età in secondi (0 = verificato ora; null = VIES non coinvolto). Con una risposta in cache checks.vies.source = cache e checks.vies.cache_scope = instance, inflight o shared. Se la risposta arriva da un'altra istanza (shared), name_match/address_match sono null, perché nome e indirizzo VIES non vengono mai salvati fuori dalla memoria dell'istanza. Se la cache non è disponibile, il VIES viene interrogato direttamente.

Chiave gratuita

POST /api/v0/keys con un oggetto JSON vuoto. L'email non è obbligatoria. La chiave viene restituita una sola volta; nello store resta solo l'hash. La quota gratuita è di 20 chiamate validate_vat al giorno, con reset a mezzanotte Europe/Rome. L'emissione è limitata per IP (l'indirizzo è conservato solo come hash, mai in chiaro) e può essere rifiutata se il budget giornaliero di letture è alto (HTTP 503) o se è stato raggiunto il tetto giornaliero di emissioni (HTTP 429).

Esempi curl

# 1. Chiave gratuita (mostrata una sola volta)
curl -sS -X POST https://aether-layer.com/api/v0/keys \
  -H 'content-type: application/json' \
  --data '{}'

# 2. Validazione di una partita IVA (POST)
curl -sS -X POST https://aether-layer.com/api/v0/validate \
  -H "Authorization: Bearer $API_KEY" \
  -H 'content-type: application/json' \
  --data '{"vat_id":"IT00905811006","name":"Eni SpA","address":"Piazzale Enrico Mattei, 1 - 00144 Roma (RM)"}'

# 3. Stesso controllo in GET (niente nome/indirizzo nella query string)
curl -sS 'https://aether-layer.com/api/v0/validate?vat_id=IT00905811006' \
  -H "Authorization: Bearer $API_KEY"

Risposta (risposta VIES reale registrata l'11 novembre 2026 per IT00905811006, ripassata nel codice del server; disclaimer e note accorciati):

{
  "ok": true,
  "schema": "aether.vat.validate.v1",
  "vat_id": "IT00905811006",
  "country": "IT",
  "number": "00905811006",
  "status": "valid",
  "invalid_reason": null,
  "checks": {
    "local": {
      "ok": true,
      "algorithm": "it.iva (office code + luhn)",
      "failure": null
    },
    "vies": {
      "queried": true,
      "source": "live",
      "result": "valid",
      "error": null,
      "attempts": 1,
      "latency_ms": 652,
      "request_date": "2026-11-11T17:19:36.118Z",
      "cache_scope": null,
      "age_seconds": 0,
      "cache_ttl_seconds": 86400
    }
  },
  "name_match": true,
  "address_match": true,
  "match_method": "aether.vat.match.v1",
  "last_known": null,
  "notes": [
    {
      "country": "IT",
      "code": "vies_opt_in_it",
      "text": "Italy: inclusion in the VIES archive is an option of the tax…",
      "source": "…"
    }
  ],
  "latency_budget_ms": 8000,
  "checked_at": "2026-11-11T17:19:36.770Z",
  "cache_age_s": 0,
  "disclaimer": "…",
  "quota": {
    "daily": 20,
    "used": 1,
    "remaining": 19,
    "window": "2026-11-11",
    "timezone": "Europe/Rome"
  },
  "outcome_id": "lo_…"
}

Quando lo Stato membro è sovraccarico (errore VIES reale registrato lo stesso giorno per FR):

{
  "ok": true,
  "schema": "aether.vat.validate.v1",
  "vat_id": "FR59542051180",
  "country": "FR",
  "number": "59542051180",
  "status": "unverified_ms_down",
  "invalid_reason": null,
  "checks": {
    "local": {
      "ok": true,
      "algorithm": "fr.tva (SIREN luhn + key mod 97 / alphanumeric key)",
      "failure": null
    },
    "vies": {
      "queried": true,
      "source": "live",
      "result": "error",
      "error": "MS_MAX_CONCURRENT_REQ",
      "scope": "member_state",
      "retryable": true,
      "retry_after_s": 600,
      "attempts": 2,
      "max_attempts": 2,
      "latency_ms": 864,
      "cache_scope": null,
      "age_seconds": 0,
      "cache_ttl_seconds": 600
    }
  },
  "last_known": null,
  "notes": [],
  "latency_budget_ms": 8000,
  "checked_at": "2026-11-11T17:19:42.864Z",
  "cache_age_s": 0,
  "disclaimer": "…",
  "quota": {
    "daily": 20,
    "used": 1,
    "remaining": 19,
    "window": "2026-11-11",
    "timezone": "Europe/Rome"
  },
  "outcome_id": "lo_…"
}

Input malformato o prefisso non supportato: HTTP 400 con error = invalid_input.

Confronto di nome e indirizzo

I campi facoltativi name e address (al massimo 200 caratteri ciascuno) vanno solo nel body JSON della POST o negli argomenti MCP, mai nella query string (HTTP 400). Vengono confrontati in locale con il dato VIES e non vengono inviati al VIES, né registrati né salvati. La risposta contiene solo name_match / address_match (true, false, oppure null se il VIES non ha il dato, per esempio in Germania, o non ha risposto) e match_method = aether.vat.match.v1.

Normalizzazione: accenti rimossi, minuscole, punteggiatura e a capo diventano spazi, spazi compattati, lettere singole unite (S.p.A. → spa); nei nomi si tolgono le forme societarie all'inizio o alla fine (srl, spa, gmbh, ag, sarl, sa, bv, ltd, sp. z o.o., …). name_match: nomi normalizzati uguali. address_match: gli insiemi di parole si sovrappongono almeno al 75% (Jaccard); le abbreviazioni non vengono espanse.

In alcuni Paesi il VIES è opt-in

Un "invalid" del VIES significa che il numero non è nei dati VIES dello Stato membro: può non esistere, non essere abilitato alle operazioni intracomunitarie o avere una registrazione non ancora completata (La tua Europa). In Italia l'inclusione nell'archivio VIES è un'opzione del contribuente (art. 35 DPR 633/1972, Agenzia delle Entrate): una partita IVA attiva può non essere nel VIES. In Spagna un numero compare nel VIES solo dopo l'iscrizione al ROI (AEAT). Le risposte per IT ed ES riportano questa nota in notes.

Stato del VIES

GET https://aether-layer.com/vies-status?window=1d|7d (senza chiave) restituisce, per Stato membro, i tentativi VIES fatti da questo gateway: tasso di disponibilità, conteggio degli esiti e latenza p50/p95. Solo aggregati: nessuna partita IVA.

Campo client facoltativo

Puoi identificare la tua integrazione con client, formato <nome>/<versione>, per esempio shopware-plugin/0.1: nel nome lettere minuscole, cifre, ., _ e -; la versione inizia con una cifra; al massimo 48 caratteri. Va nel body JSON o come parametro della query di /api/v0/validate (e come argomento del tool MCP). Un valore non valido restituisce HTTP 400. Serve solo a contare l'adozione per integrazione.

Proof-of-work facoltativo su POST /api/v0/keys

Senza proof-of-work si emettono al massimo 3 chiavi al giorno per indirizzo IP (per rete /64 in IPv6). Gli shop su hosting condiviso possono ottenere fino a 20 chiavi al giorno dallo stesso indirizzo inviando un proof-of-work nell'header X-Keys-PoW, valore <unix_ts>.<nonce>. È valido se sha256(aether-keys-pow-v1|<ts>|<nonce>) inizia con 20 bit a zero, se ts è entro 300 secondi dall'orologio del server e se il valore non è già stato usato. Il tetto giornaliero di emissione del servizio resta valido.

Calcolo del valore in JavaScript (Node.js), poi uso con curl insieme al campo client:

import { createHash } from "node:crypto";

// 20 bit iniziali a zero in sha256("aether-keys-pow-v1|<ts>|<nonce>"): circa un milione di hash.
const ts = Math.floor(Date.now() / 1000);
let nonce = 0;
for (;; nonce++) {
  const h = createHash("sha256").update(`aether-keys-pow-v1|${ts}|${nonce}`).digest();
  if (h[0] === 0 && h[1] === 0 && h[2] < 16) break;
}
console.log(`${ts}.${nonce}`); // valore dell'header X-Keys-PoW, valido 300 s, usabile una sola volta
curl -sS -X POST https://aether-layer.com/api/v0/keys \
  -H 'content-type: application/json' \
  -H "X-Keys-PoW: $POW" \
  --data '{}'

curl -sS -X POST https://aether-layer.com/api/v0/validate \
  -H "Authorization: Bearer $API_KEY" \
  -H 'content-type: application/json' \
  --data '{"vat_id":"DE143454214","client":"shopware-plugin/0.1"}'

MCP

La stessa chiave autorizza validate_vat (argomenti vat_id, facoltativi country e client) e agent_readiness_check sull'endpoint Streamable HTTP. tools/list non richiede la chiave. Nome nel registro MCP: com.aether-layer/eu-vat-validate.

{
  "mcpServers": {
    "aether-layer": {
      "url": "https://aether-layer.com/api/mcp",
      "headers": {
        "Authorization": "Bearer $API_KEY"
      }
    }
  }
}

Limiti

Licenze

Le regole di formato e checksum sono un port in TypeScript di python-stdnum (Copyright (C) 2010-2026 Arthur de Jong and others), distribuito con licenza GNU LGPL versione 2.1 o successiva (LGPL-2.1-or-later, testo della licenza). Il port è un modulo separato del gateway con un proprio NOTICE.