akwitoEU VAT proof

POST /v1/checks

Creates a verification. country can be omitted if vat_number already carries the two-letter ISO country prefix (e.g. "DE811907980").

json
{
  "vat_number": "DE 811907980",
  "country": "DE",
  "name": "Bundeszentralamt für Steuern",
  "address": { "street": "An der Küppe 1", "postcode": "53225", "city": "Bonn" },
  "refresh": false
}

mode is inferred from the request, not passed explicitly: it is verify when name is present, lookup otherwise. address is only meaningful in verify mode. Unknown JSON fields are rejected with 400 BAD_REQUEST.

FieldRequiredMeaning
vat_numberyesthe number, any formatting
countrynotwo-letter code, only when the number has no prefix
name, addressnoidentity to confirm; name switches the check to verify mode
refreshnobypass the 24 hour deduplication
referencenoyour own reference (invoice number, customer id…): trimmed, 1 to 128 characters, no control characters, otherwise 400 BAD_REQUEST. Returned as reference, printed on the PDF and in the audit file index. Two checks of one number with different references are two checks.
requester_vat_numbernoVAT number of the entity on whose behalf VIES is consulted (a group's invoicing entity). Overrides the account's requester number for this check only; 400 INVALID_REQUESTER when it is not a valid number.
max_wait_msnohow long you are willing to wait, 500 to 60 000 ms. Past it, the request is accepted as 202 and finished in the background — see Checking at checkout. Absent or 0: the server's own patience (up to 48 s when a member state is saturated).
header Idempotency-Keyno1 to 255 visible characters; replays the same response for 24 hours — see Idempotency
sh
curl -X POST https://api.akwito.eu/v1/checks \
  -H "Authorization: Bearer ak_live_…" -H "Idempotency-Key: invoice-2026-0417" -H "Content-Type: application/json" \
  -d '{"vat_number":"DE811907980","reference":"2026-0417","requester_vat_number":"FR40303265045"}'

A request with a vat_number that fails the local check — the country's format, or its check digits where akwito computes them (see GET /v1/format/{number}) — is not an HTTP error: it is recorded and returned as a check with valid: false, confidence: "invalid", company: null and the FORMAT_INVALID or CHECKSUM_INVALID warning, without calling any upstream service. It is a stored proof and counts against the quota like any check; to test a number for free, use GET /v1/format/{number}.

Deduplication #

Within a 24 hour window, an identical request (same account, same normalized VAT number, same mode, same reference (or none), same requester_vat_number (or none, the account default), and — in verify mode — the same candidate name and address) returns the previously stored check instead of running a new lookup:

  • Status 200 OK instead of 201 Created.
  • Header X-Akwito-Deduplicated: true.
  • The response body is byte-for-byte the response that was stored the first time.
  • The monthly quota is not consumed.

Pass "refresh": true in the request to bypass deduplication and force a fresh lookup.

The monthly quota is checked before deduplication, not after: once the quota is exhausted, a request that would have been served from the 24 hour deduplication window is still refused with 429 QUOTA_EXCEEDED.

Pending validation (202 Accepted) #

When VIES cannot be reached — or keeps reporting its member-state backend as saturated past the retry budget — and no cached validity is available, the request is accepted instead of being refused. The API records a check request, replays it in the background until VIES answers, and returns what the national registries already know about the company:

  • Status 202 Accepted, header Retry-After: 60.
  • status is pending, request_id identifies the background request.
  • provisional.confidence is pending and provisional.company has exactly the same shape as company in a check.
  • warnings contains VALIDATION_PENDING.
  • checks lists the per-source outcomes of the attempt.
  • poll_url is the absolute URL of GET /v1/requests/{id}.

The same 202 is returned, on request, when the check takes longer than the caller is willing to wait: see max_wait_ms and Checking at checkout. The first line of the attempt log then reads MAX_WAIT rather than an upstream error — VIES was not down, you were in a hurry.

No check is created and no quota is consumed at this point: the monthly quota is charged when the background attempt succeeds and the check is written. An identical pending request (same account, VAT number, mode and candidate) is returned as is, with the header X-Akwito-Deduplicated: true.

provisional is never a proof: valid is unknown, not true. The pending confidence level exists only in a 202 response and in GET /v1/requests/{id} — never in a stored check, never on a PDF certificate.

json
{
  "request_id": "req_01J9ZK8H2Q3M4N5P6R7S8T9V0W",
  "status": "pending",
  "retry_after": 60,
  "vat_number": "FR81775670417",
  "country": "FR",
  "mode": "lookup",
  "provisional": {
    "confidence": "pending",
    "company": {
      "name": { "value": "LVMH MOET HENNESSY LOUIS VUITTON", "source": "frentreprises", "status": "found" }
    }
  },
  "checks": [
    { "source": "vies", "outcome": "upstream_error", "error": "MS_MAX_CONCURRENT_REQ", "cached": false },
    { "source": "frentreprises", "outcome": "ok", "cached": false }
  ],
  "warnings": ["VALIDATION_PENDING"],
  "poll_url": "https://api.akwito.example/v1/requests/req_01J9ZK8H2Q3M4N5P6R7S8T9V0W"
}

Response #

The response shape is always the same regardless of mode; every key is always present. company is null when valid is false.

json
{
  "check_id": "chk_01J9X0000000000000000000",
  "checked_at": "2026-09-10T14:02:11Z",
  "vat_number": "DE811907980",
  "country": "DE",
  "mode": "verify",
  "valid": true,
  "confidence": "confirmed",
  "company": {
    "name":        { "value": "Bundeszentralamt für Steuern", "source": "client_supplied", "confirmed_by": "vies", "status": "match" },
    "address":     { "value": { "street": "An der Küppe 1", "postcode": "53225", "city": "Bonn" }, "source": "client_supplied", "confirmed_by": "vies", "status": "match" },
    "registry_id": { "value": null, "source": null, "status": "absent" },
    "legal_form":  { "value": null, "source": null, "status": "absent" },
    "active":      { "value": null, "source": null, "status": "absent" },
    "activity":    { "value": null, "source": null, "status": "absent" },
    "lei":         { "value": "529900D6BF99LW9R2E68", "source": "gleif", "status": "found" },
    "parent":      { "value": { "name": "Holding AG", "lei": "5299009ERJU8YHLHEN31" }, "source": "gleif", "status": "found" }
  },
  "checks": [
    { "source": "vies", "outcome": "ok", "ref": "WAPI1234", "cached": false },
    { "source": "owndb", "outcome": "absent", "cached": false },
    { "source": "gleif", "outcome": "ok", "cached": false },
    { "source": "viesapprox", "outcome": "ok", "cached": false,
      "fields": { "name": "match", "street": "match", "postcode": "match", "city": "match" } }
  ],
  "warnings": [],
  "proof_url": "https://api.akwito.example/v1/checks/chk_01J9X0000000000000000000",
  "reference": "F-2026-0417",
  "requester": { "country": "FR", "number": "40303265045" }
}

reference is only present when it was given.

requester ({"country", "number"}) is present whenever a requester number was sent to VIES, from the account settings or from requester_vat_number; the VIES ref is tied to it.

201 Created for a fresh check, 200 OK when the response comes from deduplication (with the X-Akwito-Deduplicated header set).

company fields #

Each field of company follows the same shape:

KeyMeaning
valueThe field's value (a string, an address object, a boolean…), or null if absent
sourceThe source that provided the value (vies, frentreprises, gleif, eori, owndb, client_supplied), or null
confirmed_byPresent only when a verify/candidate value was checked against an official source and matched it; currently always "vies" when set
statusfound, absent, match, mismatch, or not_processed

confirmed_by semantics: it is set only on the name and address fields, only when a confirm pass actually ran (candidate present, viesapprox called) and that field's status came back match. It is absent on mismatch and not_processed — a compared-but-unconfirmed field still shows its real status, just without confirmed_by. Its presence means "this value was checked against the official record from confirmed_by and matched", not that the value itself came from that source.

The eight company fields are: name, address, registry_id, legal_form, active, activity, lei, parent.

parent is the ultimate parent of the company as declared in the LEI system: its value is { "name", "lei" }. It comes from GLEIF's ultimate-parent relationship, so it is only ever found when the company itself has an LEI; absent means no parent is declared (the company heads its group, has no LEI, or has not consented to publish the relationship). The key is omitted altogether on checks created before 23 September 2026.

registry_id is the identifier in the national company register: the SIREN in France (frentreprises), and elsewhere the register entry GLEIF records for the LEI holder — HRB 719915 for a German company, say — with the same "LEI holders only" caveat as parent.

checks (per-source outcomes) #

Each entry in checks reports what one source did for this check:

KeyPresent whenMeaning
sourcealwayssource identifier (vies, viesapprox, frentreprises, gleif, eori, owndb)
outcomealwayssee outcome values below
refoutcome ok (when available)upstream request identifier (VIES requestIdentifier)
atwhen availabletimestamp of the underlying fetch
cachedalwaystrue if served from cache instead of a live call
reasonoutcome skippedwhy the source was not called
erroroutcome upstream_errorupstream error code
served_from_cacheoutcome upstream_error (stale fallback)true if a stale cached value was served instead
cache_datesame as aboveage of the stale value that was served
fieldssource viesapproxper-field confirm result: match, mismatch, or not_processed, keyed by name, street, postcode, city

ref is the VIES requestIdentifier, the only element the Commission ties to the caller's own company. VIES returns it only when the caller sends its own VAT number, so it is present only if the account has a requester VAT number — the one asked for at sign-up and editable in the dashboard settings. Without it the check is still performed, but the PDF certificate states that the consultation is not linked to your company in the Commission's records.

Outcome values: ok, absent, skipped, upstream_error, timeout, circuit_open. Values of reason on a skipped outcome: not_applicable, unknown_source, cancelled, and backpressure (our own per-member-state outbound concurrency limit was saturated, so the source was never called — this is never charged to the source's circuit breaker).

Confidence levels #

Rules are evaluated in this order; the first that matches wins.

LevelCondition
invalidthe format is invalid, or VIES answers that the number is invalid
degradedvalidity was served from a stale cache because the upstream was unreachable
confirmedvalid, and either the name came from an official source (vies, frentreprises, or eori) and was not contradicted by a confirm name mismatch, or a confirm pass matched the name and at least one address sub-field
probablevalid, and either a name from owndb or gleif was not contradicted by a confirm mismatch, or a confirm pass matched the name without matching any address sub-field
unverifiedvalid, but no identity could be established, or the name (from any source, official or secondary) was contradicted by a confirm mismatch

Warnings catalogue #

warnings is an array of stable string codes; it is [] when there is nothing to flag.

CodeMeaning
FORMAT_INVALIDthe VAT number does not match the expected format for its country
CHECKSUM_INVALIDthe format is right but the check digits are wrong: no such number can exist. Computed for FR, DE, ES, IT, BE, NL, PT and AT only
DE_NO_TRADER_DATAVIES answered valid for a DE number but did not return a trader name (Germany does not expose trader data through VIES)
ES_NO_TRADER_DATAsame as above, for ES
VIES_UNAVAILABLEVIES was unreachable and the validity shown was served from a stale cache
NAME_MISMATCHthe candidate name was compared against the official record and did not match
ADDRESS_MISMATCHat least one candidate address sub-field was compared and did not match
QUALIFIED_CHECK_NOT_SUPPORTEDa confirm pass was attempted but the upstream did not process it for any field (not_processed across the board)
GLEIF_NAME_MATCH_ONLYthe name comes from GLEIF outside France, i.e. matched by legal name only, not by national company registry identifier
EORI_NO_CONSENTthe EORI holder has not consented to releasing their name and address

Country coverage #

Countryvalidatelookupconfirm
FRviesfrentreprises, gleifviesapprox
DEviesowndb, gleifviesapprox
ESviesowndb, eori, gleifviesapprox
BE, ITvieseoriviesapprox
default (any other member state)vies—viesapprox

GLEIF does not index VAT numbers directly: outside France (where the French company register SIREN is used), GLEIF is only queried when a candidate name is supplied, matched by exact normalized legal name — never by VAT number alone, and never fuzzy-matched.