Skip to content
VAT API

Reference · v1

API documentation

Validate EU VAT numbers. Every uncached lookup races all sources in parallel and returns the first definitive answer. JSON over HTTPS. The machine readable spec is at /openapi.json (OpenAPI 3.1).

Quick start

Get a free API key, then:

curl -H "Authorization: Bearer $VAT_API_KEY" \
  "https://vatapi.be/api/v1/vat/BE0684579082"

Authentication

Send your key as a bearer token: Authorization: Bearer YOUR_API_KEY. Keys are created and rotated in your dashboard. A missing or invalid key returns 401. Never put your key in client-side code.

Check a VAT number

GET /api/v1/vat/{vatNumber}

Input is forgiving: BE 0684.579.082, be0684579082 and 0684579082?country=BE are equivalent. GR is treated as EL, XI (Northern Ireland) is supported, and GB returns 422 because the UK is not covered by VIES.

QueryDescription
countryCountry code when the number has no prefix.
fresh=1Skip stored results younger than 24 hours and check live.
requesterYour own VAT number. VIES then returns a consultation number as proof.

Example response

200 OK
{
    "vat_number": "BE0684579082",
    "country_code": "BE",
    "number": "0684579082",
    "valid": true,
    "name": "BV MILJAAR",
    "address": "Bekkemmeers 26\n8740 Pittem",
    "consultation_number": null,
    "check_id": 18342,
    "checked_at": "2026-11-05T12:02:50Z",
    "served_from": "live",
    "stale": false,
    "conflict": false,
    "source": "vies_soap",
    "sources": [
        {
            "source": "vies_rest",
            "status": "error",
            "error": "MS_MAX_CONCURRENT_REQ",
            "latency_ms": 412,
            "checked_at": "2026-11-05T12:02:49Z",
            "last_success_at": "2026-11-05T11:58:02Z",
            "started_at_ms": 3,
            "finished_at_ms": 415
        },
        {
            "source": "vies_soap",
            "status": "ok",
            "valid": true,
            "latency_ms": 640,
            "checked_at": "2026-11-05T12:02:50Z",
            "last_success_at": "2026-11-05T12:02:50Z",
            "started_at_ms": 3,
            "finished_at_ms": 643
        },
        {
            "source": "kbo_ws",
            "status": "skipped",
            "reason": "not_applicable"
        },
        {
            "source": "vatcomply",
            "status": "pending",
            "started_at_ms": 4,
            "finished_at_ms": null
        },
        {
            "source": "controleerbtwnummer",
            "status": "inconclusive",
            "latency_ms": 380,
            "checked_at": "2026-11-05T12:02:49Z",
            "last_success_at": "2026-11-05T12:01:13Z",
            "started_at_ms": 4,
            "finished_at_ms": 384
        },
        {
            "source": "kbo_open_data",
            "status": "ok",
            "valid": null,
            "found": true,
            "active": true,
            "name": "BV MILJAAR",
            "address": "Bekkemmeers 26\n8740 Pittem",
            "data_date": "2026-11-01",
            "latency_ms": 2,
            "checked_at": "2026-11-05T12:02:49Z",
            "last_success_at": "2026-11-05T12:02:49Z",
            "started_at_ms": 4,
            "finished_at_ms": 6
        }
    ],
    "registry": null
}

Response fields

FieldTypeDescription
vat_numberstringNormalized number with country prefix, e.g. BE0684579082. Greece uses EL.
country_codestringTwo letter country code.
numberstringThe number without the prefix.
validboolean | nulltrue or false when a source gave a definitive answer. null when none could and nothing is cached.
namestring | nullRegistered name, when the source provides it. VIES placeholders ("---") become null.
addressstring | nullRegistered address, lines separated by \n.
consultation_numberstring | nullOfficial VIES proof of the check. Only present on live answers when you pass requester.
checked_atstring | nullISO 8601 UTC time of the definitive answer.
served_fromstringlive, cache, stale_cache or none. See Cache and stale results.
stalebooleantrue when the stored result is older than 24 hours. It is refreshed in the background.
conflictbooleantrue when sources that can confirm both valid and invalid disagreed. The verdict of the highest-ranked source is kept.
check_idinteger | nullId of this check, useful when you contact us about a result.
sourcestring | nullWhich source gave the answer: vies_rest, vies_soap, kbo_ws, vatcomply or controleerbtwnummer.
sourcesarrayWhat each source did during this request, including started_at_ms and finished_at_ms offsets. See Sources.
registryobject | nullBelgian KBO registry data (BE only). Never sets valid. Carries attribution, notice and data_date.
reasonstringOnly present as "format" when the number failed the local syntax check. No source was called.
errorobjectOnly present when valid is null: {code, message}. Retry after the Retry-After header.

Sources

On every uncached lookup all applicable sources start at the same moment. The response is sent as soon as one gives a definitive answer: valid: true from any source, or valid: false from VIES REST, VIES SOAP or vatcomply. Sources still running are reported as pending and finish in the background; afterwards the stored record is merged by authority (VIES, then KBO, then vatcomply, then controleerbtwnummer.eu).

  1. VIES (REST) vies_rest Can confirm valid and invalid
  2. VIES (SOAP) vies_soap Can confirm valid and invalid
  3. KBO Public Search kbo_ws Can only confirm valid
  4. vatcomply vatcomply Can confirm valid and invalid
  5. controleerbtwnummer.eu controleerbtwnummer Can only confirm valid
  6. KBO open data kbo_open_data Register lookup, no VAT verdict

Source statuses

ok
The source answered. valid tells you what it said.
error
The source failed. error holds a short code, see Errors.
inconclusive
The source answered but cannot decide. controleerbtwnummer.eu and the KBO webservice only confirm valid numbers: a negative answer is never treated as invalid.
skipped
The source was not called. reason explains why.
pending
The source was still running when we answered. It finishes in the background and its answer enriches the stored record.
not_checked
Only on cache hits: this source has never been called for this number.

Skip reasons

not_applicable
Not applicable to this country
disabled
Disabled, no credentials configured
circuit_open
Paused after repeated failures
member_state_unavailable
VIES reports this member state as offline
vies_unavailable
VIES reports the whole service as offline
quota
Daily quota almost used up
not_imported
No KBO open data has been imported yet
suppressed
Details withheld: the trader objected to their processing

Before any source is called, a local syntax check (country format, Belgian checksum) runs. A failing number returns valid: false, reason: "format" without calling anything.

Belgian registry data. For Belgian numbers the registry block carries data from the KBO/BCE (FOD Economie), provided without warranty. It never changes valid. Direct marketing use is prohibited and there is no search endpoint.

Cache and stale results

  • served_from: "cache", stale: false: a stored result younger than 24 hours. No source is called. source and checked_at say where and when it came from.
  • served_from: "cache", stale: true: a stored result older than 24 hours, returned immediately. A background job races every source to refresh it, so the next lookup gets the update.
  • served_from: "live": the number was not stored (or you sent fresh=1), so the race ran and the first definitive answer was returned.
  • served_from: "stale_cache", stale: true: a live check ran (for example with fresh=1) but no source could decide, so the stored result is returned as a fallback.
  • served_from: "none" with valid: null: nothing could answer and nothing is stored. The response is still HTTP 200 with a Retry-After header. Retry later.

Need a live answer no matter what, for example right before issuing an invoice? Add fresh=1. It counts the same towards your limits.

Errors

Errors always look like {"error": {"code": "...", "message": "..."}}, also for 404, 405 and 500.

HTTPcodeMeaning
401unauthenticatedMissing or invalid API key.
404not_foundUnknown endpoint.
405method_not_allowedWrong HTTP method.
422country_not_coveredThe country (GB) is not covered by VIES.
422country_unknownNo country prefix and no country parameter.
422invalid_vat_numberThe input is empty or contains invalid characters.
422invalid_requesterThe requester number could not be understood.
422validation_failedA query parameter is invalid.
429rate_limitedRate limit reached. See Retry-After.
500server_errorSomething went wrong on our side.

Source error codes

These appear as error inside sources[], not as HTTP errors.

MS_MAX_CONCURRENT_REQ
VIES busy for this country (too many requests)
GLOBAL_MAX_CONCURRENT_REQ
VIES busy (too many requests overall)
MS_UNAVAILABLE
Member state system offline
SERVICE_UNAVAILABLE
VIES service unavailable
TIMEOUT
Timed out
timeout
Timed out
connection_error
Could not connect to the provider
INVALID_INPUT
The provider rejected the input
INVALID_REQUESTER_INFO
The requester number was rejected
malformed_response
Unreadable response from the provider
unknown_error
Unknown provider error
soap_fault
Provider returned a SOAP fault
auth_failed
Credentials rejected (our configuration)
quota_exhausted
Daily request quota used up
terms_not_accepted
Provider terms of service not accepted
unexpected_error
Unexpected internal error
http_NNN
The provider answered with HTTP status NNN.

Rate limits

Each account gets 60 requests per minute and 5,000 per day, regardless of how many keys you create. Over the limit you receive 429 with a Retry-After header (seconds) and X-RateLimit-Limit / X-RateLimit-Remaining.

Status endpoint

GET /api/v1/status needs no key. It returns per source the last success, the error rate and attempts of the last hour, the circuit breaker state, and the availability of every VIES member state. The same data powers the status page.

GET /api/v1/stats needs no key either. It returns measured response times of the last 7 days (p50/p95 for live and cached answers, per-source latency and error rate, which source won the race) with an advertise flag that is only true once the sample is large enough.

Changelog

The API is versioned in the path (/api/v1). Additive changes, such as new fields or new sources, do not create a new version, so ignore fields you do not know.

2026-11-05 · v1.1

Sources now race in parallel: first definitive answer wins, the rest finish in the background. New fields: conflict, check_id, sources[].started_at_ms / finished_at_ms and the pending source status.

2026-11-05 · v1

First release: VIES REST and SOAP, KBO Public Search, vatcomply and controleerbtwnummer.eu fallbacks, 24 hour cache with stale fallback, Belgian registry block, status endpoint.