// Server-only — calls the European Commission's public VIES REST API to // confirm an EU VAT ID is actually registered, not just correctly // formatted (see lib/vatId.ts's own comment: format alone is never // enough to zero-rate an invoice). Confirmed live and working against // the real endpoint 2026-07-23 (POST {countryCode, vatNumber} → // {valid: boolean, ...}) — this is the Commission's own documented REST // API, not a guess. const VIES_URL = "https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number"; export type ViesCheckResult = | { ok: true; valid: boolean; name: string | null; address: string | null } | { ok: false; reason: string }; // `vatNumber` must NOT include the country prefix (VIES wants it split // out) — callers pass the full "DE123456789"-shaped id and this function // does the splitting, since every call site already has the normalized // full id (see lib/vatId.ts's normalizeVatId()) rather than the two parts // separately. export async function checkVatIdViaVies(vatId: string): Promise { const countryCode = vatId.slice(0, 2); const vatNumber = vatId.slice(2); if (!countryCode || !vatNumber) return { ok: false, reason: "Ungültiges USt-IdNr.-Format." }; try { // 8s timeout — VIES is a shared EU-wide government service with no // uptime SLA to this shop; a slow/unreachable response must not hang // checkout indefinitely. Callers treat `ok: false` as "couldn't // confirm" and fail closed (no exemption), never as "confirmed invalid". const res = await fetch(VIES_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ countryCode, vatNumber }), signal: AbortSignal.timeout(8000), }); if (!res.ok) return { ok: false, reason: `VIES antwortete mit ${res.status}` }; const data: { actionSucceed?: boolean; valid?: boolean; name?: string; address?: string; errorWrappers?: { error?: string }[] } = await res.json(); // VIES answers 200 even when it couldn't actually perform the check — // `actionSucceed: false` (e.g. `MS_UNAVAILABLE`, the member state's own // national gateway being temporarily down — Germany's in particular is // known to do this) means "couldn't confirm", not "confirmed invalid". // Without this check a `MS_UNAVAILABLE` response fell through to // `Boolean(data.valid)` on a body that has no `valid` field at all, // silently reading as `valid: false` — a real, currently-registered VAT // ID would then look rejected instead of "VIES unavailable, try again". if (data.actionSucceed === false) { const reason = data.errorWrappers?.[0]?.error ?? "VIES konnte die Anfrage nicht bearbeiten."; return { ok: false, reason: `VIES: ${reason}` }; } return { ok: true, valid: Boolean(data.valid), name: data.name && data.name !== "---" ? data.name : null, address: data.address && data.address !== "---" ? data.address : null, }; } catch (err) { return { ok: false, reason: err instanceof Error ? err.message : "VIES ist gerade nicht erreichbar." }; } }