BZSt §18e qualified VAT confirmation (German legal evidence)
Obtain a qualifizierte Bestätigungsmitteilung from Germany's BZSt with a single API call. Field-by-field match codes, 10-year evidence retention, idempotent retries.
German sellers making VAT-free intra-EU supplies are required by §18e UStG to obtain a qualifizierte Bestätigungsmitteilung, a confirmation from the Bundeszentralamt für Steuern (BZSt) that the foreign customer's VAT is valid and that the specific company details on file match what the seller has in their books. This is the legal evidence that protects the seller's good-faith position if the customer's VAT is later found to be fraudulent.
vatverify's POST /v1/confirm wraps BZSt's eVatR endpoint and stores the
result for 10 years. Every developer-first VAT API on the market today is
VIES-only; vatverify is the first to expose BZSt as a first-class REST
resource.
Who needs this
- You're a German-resident seller with
requester_vat_numberissued by a German Finanzamt (formatDExxxxxxxxx). - You ship or invoice EU customers without charging VAT (reverse charge).
- Your tax advisor or an auditor will eventually ask for per-field evidence, not just "VIES returned valid."
If you're not German-resident, VIES validation (POST /v1/validate or
/v1/validate/batch) is what you want; §18e doesn't apply.
Quickstart
Business-plan API key required. If you registered a default requester VAT
on your key you can omit requester_vat_number below.
curl
curl -X POST "https://api.vatverify.dev/v1/confirm" \
-H "Authorization: Bearer $VATVERIFY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 00000000-0000-4000-8000-000000000001" \
-d '{
"vat_number": "FR44732829320",
"company": {
"name": "Airbus SAS",
"street": "2 Rond-Point Emile Dewoitine",
"postcode": "31700",
"town": "Blagnac"
}
}'Node.js SDK
import { Vatverify } from "@vatverify/node"
const vat = new Vatverify()
const result = await vat.confirm({
vat_number: "FR44732829320",
company: {
name: "Airbus SAS",
street: "2 Rond-Point Emile Dewoitine",
postcode: "31700",
town: "Blagnac",
},
}, {
idempotency_key: "00000000-0000-4000-8000-000000000001",
})
result.data.qualified // true when every requested field matches
result.data.matches.name // 'A' | 'B' | 'C' | 'D'
result.data.confirmation_id // retrievable for 10 years
result.meta.bzst_status_code // 'evatr-0000', 'evatr-2001', 'evatr-2002', ...Interpreting the four match codes
For each of name, street, postcode, town BZSt returns one of:
| Code | Meaning |
|---|---|
A | Matches: BZSt's check against the foreign member state came back positive. |
B | Does not match: the field is wrong. |
C | Not requested: you did not supply this field in the request. |
D | Not provided: the foreign EU member state doesn't return this field (varies by country). |
qualified: true only when every requested field returned A. If BZSt
returns evatr-0000 but one or more fields come back B or D, you get
valid: true, qualified: false. The record still has legal weight (you can
show you tried to confirm), but the seller should follow up with the customer
to get the matching details.
Idempotency
Confirmations are legal evidence. A retried POST under network flakes must
not mint a second row with a different confirmation_id. Pass an
Idempotency-Key header (any UUID); retries with the same key within 24
hours return the originally stored confirmation.
await vat.confirm(input, { idempotency_key: randomUUID() })Retrieving a stored confirmation
const { data } = await vat.confirmations.get(result.data.confirmation_id)
data.qualified // original outcome
data.matches // original field-match grid
data.bzst_status_code // original evatr-XXXX
data.created_at // original BZSt call timestampConfirmations are scoped to the API key that created them; they remain readable even after the plan is downgraded. Retention is 10 years.
Error codes
| HTTP | error.code | When |
|---|---|---|
| 200 | — (data.valid: false) | BZSt answered but the VAT isn't valid right now: evatr-2001 (not issued), evatr-2002 (valid in future, see valid_from), evatr-2006 (was valid, see validity window). |
| 400 | invalid_format | VAT format invalid, checksum failed, or BZSt returned evatr-0002/0004/0005/0012/2003. |
| 402 | plan_required | Not on Business plan. |
| 422 | invalid_requester_vat | Requester VAT isn't authorised (evatr-0006, 0007, 2005). |
| 429 | rate_limited | Your burst rate limit. |
| 429 | bzst_session_limit | BZSt returned evatr-0008. Retry with backoff. |
| 502 | registry_unavailable | Network failure to BZSt. |
| 502 | bzst_server_error | BZSt server error (evatr-2004, 2011, 3011). |
| 503 | bzst_unavailable | BZSt service degraded (evatr-0011, 1001–1004). |
The Node SDK throws a dedicated class for each: BzstSessionLimitError,
BzstUnavailableError, BzstRejectedError, all extending VatverifyError.
Default requester VAT
If you always call /v1/confirm from the same German entity, set a per-key
default in Supabase and omit requester_vat_number in every request. The
dashboard UI for this ships in a follow-up; for now, ask support to set
api_keys.default_requester_vat for your key.
What confirm is NOT
- It does not validate German (
DE…) VATs. UsePOST /v1/validatefor that. - It does not replace VIES in the general case; only German sellers need it.
- It does not return a PDF. JSON is the canonical evidence; a PDF export endpoint is on the roadmap if customers ask.