Documentation / Checks
POST /v1/checks
Creates a verification. country can be omitted if vat_number already carries the two-letter ISO country prefix (e.g. "DE811907980").
{
"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.
| Field | Required | Meaning |
|---|---|---|
vat_number | yes | the number, any formatting |
country | no | two-letter code, only when the number has no prefix |
name, address | no | identity to confirm; name switches the check to verify mode |
refresh | no | bypass the 24 hour deduplication |
reference | no | your 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_number | no | VAT 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_ms | no | how 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-Key | no | 1 to 255 visible characters; replays the same response for 24 hours — see Idempotency |
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 OKinstead of201 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, headerRetry-After: 60. statusispending,request_ididentifies the background request.provisional.confidenceispendingandprovisional.companyhas exactly the same shape ascompanyin a check.warningscontainsVALIDATION_PENDING.checkslists the per-source outcomes of the attempt.poll_urlis the absolute URL ofGET /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.
{
"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.
{
"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:
| Key | Meaning |
|---|---|
value | The field's value (a string, an address object, a boolean…), or null if absent |
source | The source that provided the value (vies, frentreprises, gleif, eori, owndb, client_supplied), or null |
confirmed_by | Present only when a verify/candidate value was checked against an official source and matched it; currently always "vies" when set |
status | found, 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:
| Key | Present when | Meaning |
|---|---|---|
source | always | source identifier (vies, viesapprox, frentreprises, gleif, eori, owndb) |
outcome | always | see outcome values below |
ref | outcome ok (when available) | upstream request identifier (VIES requestIdentifier) |
at | when available | timestamp of the underlying fetch |
cached | always | true if served from cache instead of a live call |
reason | outcome skipped | why the source was not called |
error | outcome upstream_error | upstream error code |
served_from_cache | outcome upstream_error (stale fallback) | true if a stale cached value was served instead |
cache_date | same as above | age of the stale value that was served |
fields | source viesapprox | per-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.
| Level | Condition |
|---|---|
invalid | the format is invalid, or VIES answers that the number is invalid |
degraded | validity was served from a stale cache because the upstream was unreachable |
confirmed | valid, 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 |
probable | valid, 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 |
unverified | valid, 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.
| Code | Meaning |
|---|---|
FORMAT_INVALID | the VAT number does not match the expected format for its country |
CHECKSUM_INVALID | the 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_DATA | VIES answered valid for a DE number but did not return a trader name (Germany does not expose trader data through VIES) |
ES_NO_TRADER_DATA | same as above, for ES |
VIES_UNAVAILABLE | VIES was unreachable and the validity shown was served from a stale cache |
NAME_MISMATCH | the candidate name was compared against the official record and did not match |
ADDRESS_MISMATCH | at least one candidate address sub-field was compared and did not match |
QUALIFIED_CHECK_NOT_SUPPORTED | a confirm pass was attempted but the upstream did not process it for any field (not_processed across the board) |
GLEIF_NAME_MATCH_ONLY | the name comes from GLEIF outside France, i.e. matched by legal name only, not by national company registry identifier |
EORI_NO_CONSENT | the EORI holder has not consented to releasing their name and address |
Country coverage #
| Country | validate | lookup | confirm |
|---|---|---|---|
| FR | vies | frentreprises, gleif | viesapprox |
| DE | vies | owndb, gleif | viesapprox |
| ES | vies | owndb, eori, gleif | viesapprox |
| BE, IT | vies | eori | viesapprox |
| 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.