vatverify home
All guides

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_number issued by a German Finanzamt (format DExxxxxxxxx).
  • 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:

CodeMeaning
AMatches: BZSt's check against the foreign member state came back positive.
BDoes not match: the field is wrong.
CNot requested: you did not supply this field in the request.
DNot 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 timestamp

Confirmations are scoped to the API key that created them; they remain readable even after the plan is downgraded. Retention is 10 years.

Error codes

HTTPerror.codeWhen
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).
400invalid_formatVAT format invalid, checksum failed, or BZSt returned evatr-0002/0004/0005/0012/2003.
402plan_requiredNot on Business plan.
422invalid_requester_vatRequester VAT isn't authorised (evatr-0006, 0007, 2005).
429rate_limitedYour burst rate limit.
429bzst_session_limitBZSt returned evatr-0008. Retry with backoff.
502registry_unavailableNetwork failure to BZSt.
502bzst_server_errorBZSt server error (evatr-2004, 2011, 3011).
503bzst_unavailableBZSt 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. Use POST /v1/validate for 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.

Validate VAT in three lines.

Free up to 500 requests per month. No credit card.

Start free