EU-USt-IdNr.-Prüf-API

Was diese API tut

validate_vat prüft eine EU-USt-IdNr. in zwei Schritten. Zuerst wird die Eingabe normalisiert (Leerzeichen, Punkte und Bindestriche werden ignoriert, GR wird zu EL) und Format und Prüfziffer werden lokal geprüft, für die 27 Mitgliedstaaten und XI (Nordirland). Danach wird VIES, der Dienst der Europäischen Kommission, über seine offizielle REST-API live abgefragt. Name und Anschrift aus VIES werden nicht zurückgegeben (VIES-Nutzungsbedingungen): senden Sie Ihren eigenen name und/oder address, um die Booleschen Werte name_match / address_match zu erhalten, lokal berechnet.

Automatische Prüfung: lokale Format- und Prüfziffernregeln plus eine Live-Abfrage von VIES (Europäische Kommission). Keine steuerliche Bescheinigung und keine Rechtsberatung; die VIES-Daten stammen aus den Datenbanken der Mitgliedstaaten. Bewahren Sie einen eigenen Nachweis der VIES-Abfrage auf.

Status

Die API gibt nie invalid zurück, weil VIES ausgefallen ist.

Cache der VIES-Ergebnisse

VIES wertet die mehrfache Prüfung derselben Nummer am selben Tag als missbräuchliche Nutzung, deshalb werden Ergebnisse zwischengespeichert: valid/invalid 24 h, unverified/unverified_ms_down 10 min (checks.vies.retry_after_s = wann VIES wieder gefragt wird). Der Cache ist für alle Server-Instanzen gemeinsam (Firestore vies_cache, Schlüssel ist ein HMAC der Nummer, nie die Nummer selbst), und gleichzeitige Anfragen für dieselbe Nummer teilen sich einen VIES-Aufruf. checked_at ist der Zeitpunkt der ursprünglichen VIES-Prüfung, cache_age_s ihr Alter in Sekunden (0 = jetzt geprüft; null = VIES nicht beteiligt). Bei einer Antwort aus dem Cache ist checks.vies.source = cache und checks.vies.cache_scope = instance, inflight oder shared. Kommt die Antwort von einer anderen Instanz (shared), sind name_match/address_match null, weil Name und Anschrift aus VIES nie außerhalb des Instanzspeichers gespeichert werden. Ist der Cache nicht erreichbar, wird VIES direkt abgefragt.

Kostenloser Schlüssel

POST /api/v0/keys mit einem leeren JSON-Objekt. Eine E-Mail ist nicht erforderlich. Der Schlüssel wird nur einmal zurückgegeben; gespeichert wird nur der Hash. Das kostenlose Tageskontingent beträgt 20 validate_vat-Aufrufe, Reset um Mitternacht Europe/Rome. Die Ausgabe ist je IP begrenzt (die Adresse wird nur gehasht gespeichert, nie im Klartext) und kann abgelehnt werden, wenn das tägliche Lesebudget hoch ist (HTTP 503) oder die tägliche Ausgabemenge erreicht ist (HTTP 429).

curl-Beispiele

# 1. Kostenloser Schlüssel (wird nur einmal angezeigt)
curl -sS -X POST https://aether-layer.com/api/v0/keys \
  -H 'content-type: application/json' \
  --data '{}'

# 2. Eine USt-IdNr. prüfen (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. Dieselbe Prüfung mit GET (Name/Anschrift nicht in der Query-String)
curl -sS 'https://aether-layer.com/api/v0/validate?vat_id=IT00905811006' \
  -H "Authorization: Bearer $API_KEY"

Antwort (echte VIES-Antwort vom 11. November 2026 für IT00905811006, durch den Servercode abgespielt; Disclaimer und Hinweise gekürzt):

{
  "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_…"
}

Wenn der Mitgliedstaat überlastet ist (echter VIES-Fehler vom selben Tag für 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_…"
}

Fehlerhafte Eingabe oder nicht unterstütztes Präfix: HTTP 400 mit error = invalid_input. Deutschland liefert über VIES keinen Namen und keine Anschrift; name_match / address_match sind dann null.

Abgleich von Name und Anschrift

Die optionalen Felder name und address (je höchstens 200 Zeichen) gehören nur in den JSON-Body des POST oder in die MCP-Argumente, nie in die Query-String (HTTP 400). Sie werden lokal mit dem VIES-Datensatz verglichen und nicht an VIES gesendet, nicht protokolliert und nicht gespeichert. Die Antwort enthält nur name_match / address_match (true, false oder null, wenn VIES für das Feld keine Daten hat, z. B. Deutschland, oder nicht geantwortet hat) und match_method = aether.vat.match.v1.

Normalisierung: Akzente entfernt, Kleinschreibung, Satzzeichen und Zeilenumbrüche werden zu Leerzeichen, Leerzeichen zusammengefasst, Einzelbuchstaben verbunden (S.p.A. → spa); bei Namen werden Rechtsformen am Anfang oder Ende entfernt (srl, spa, gmbh, ag, sarl, sa, bv, ltd, sp. z o.o., …). name_match: normalisierte Namen sind gleich. address_match: die beiden Wortmengen überschneiden sich zu mindestens 75 % (Jaccard); Abkürzungen werden nicht aufgelöst.

In einigen Ländern ist VIES opt-in

Ein VIES-„invalid“ bedeutet, dass die Nummer nicht in den VIES-Daten des Mitgliedstaats steht: Sie existiert vielleicht nicht, ist nicht für innergemeinschaftliche Umsätze freigeschaltet oder die Registrierung ist noch nicht abgeschlossen (Ihr Europa). In Italien ist die Aufnahme in das VIES-Archiv eine Option des Steuerpflichtigen (Art. 35 DPR 633/1972, Agenzia delle Entrate); in Spanien erscheint eine Nummer erst nach Eintragung im ROI in VIES (AEAT). Antworten für IT und ES enthalten diesen Hinweis in notes.

VIES-Status

GET https://aether-layer.com/vies-status?window=1d|7d (ohne Schlüssel) liefert je Mitgliedstaat die VIES-Versuche dieses Gateways: Verfügbarkeitsquote, Anzahl der Ergebnisse und Latenz p50/p95. Nur Aggregate: keine USt-IdNrn.

Optionales Feld client

Mit client können Sie Ihre Integration kennzeichnen, Format <name>/<version>, zum Beispiel shopware-plugin/0.1: im Namen Kleinbuchstaben, Ziffern, ., _ und -; die Version beginnt mit einer Ziffer; höchstens 48 Zeichen. Im JSON-Body oder als Query-Parameter von /api/v0/validate (und als Argument des MCP-Tools). Ein ungültiger Wert ergibt HTTP 400. Das Feld dient nur dazu, die Nutzung pro Integration zu zählen.

Optionaler Proof-of-Work bei POST /api/v0/keys

Ohne Proof-of-Work werden höchstens 3 Schlüssel pro Tag und IP-Adresse ausgegeben (bei IPv6 pro /64-Netz). Shops auf geteiltem Hosting erhalten bis zu 20 Schlüssel pro Tag von derselben Adresse, wenn sie im Header X-Keys-PoW einen Proof-of-Work mit dem Wert <unix_ts>.<nonce> senden. Er ist gültig, wenn sha256(aether-keys-pow-v1|<ts>|<nonce>) mit 20 Null-Bits beginnt, ts höchstens 300 Sekunden von der Serveruhr abweicht und der Wert noch nicht verwendet wurde. Die tägliche Ausgabegrenze des Dienstes gilt weiterhin.

Berechnung des Werts in JavaScript (Node.js), danach Verwendung mit curl zusammen mit dem Feld client:

import { createHash } from "node:crypto";

// 20 führende Null-Bits in sha256("aether-keys-pow-v1|<ts>|<nonce>"): etwa eine Million Hashes.
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}`); // Wert des Headers X-Keys-PoW, 300 s gültig, nur einmal verwendbar
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

Derselbe Schlüssel autorisiert validate_vat (Argumente vat_id, optional country und client) und agent_readiness_check am Streamable-HTTP-Endpunkt. tools/list braucht keinen Schlüssel. Name in der MCP Registry: com.aether-layer/eu-vat-validate.

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

Grenzen

Lizenzen

Die Format- und Prüfziffernregeln sind eine TypeScript-Portierung von python-stdnum (Copyright (C) 2010-2026 Arthur de Jong and others), lizenziert unter der GNU LGPL Version 2.1 oder später (LGPL-2.1-or-later, Lizenztext). Die Portierung ist ein eigenes Modul des Gateways mit eigener NOTICE.