Error Codes
All API errors follow a consistent format. The HTTP status code indicates the category of error, and the response body contains a machine-readable error code and a human-readable message.
Error format
{
"error": {
"code": "NOT_FOUND",
"message": "Company not found for KVK number 99999999"
}
} Error codes
| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND | 404 | No company found for the given KVK or VAT number. |
INVALID_KVK_NUMBER | 400 | The provided KVK number is not a valid 8-digit number. |
INVALID_VAT_NUMBER | 400 | The provided VAT number does not match the expected format (e.g., NL + 9 digits + B + 2 digits). |
INVALID_QUERY | 400 | The search parameters were rejected. A postcode filter must be paired with a huisnummer and written without a space (6604LW, not 6604 LW). This is a problem with the query, not with the service. |
UNAUTHORIZED | 401 | Missing or invalid API key. Ensure you're sending a valid key via the Authorization header or apikey query parameter. |
RATE_LIMIT_EXCEEDED | 429 | You've exceeded the rate limit for your plan. Wait and try again, or upgrade your plan for higher limits. |
QUOTA_EXCEEDED | 429 | You've used all your allocated lookups/searches/validations for this month. Upgrade your plan or wait until the next billing cycle. |
UPSTREAM_ERROR | 502 | An upstream service (KVK API or VIES) is temporarily unavailable. The request may succeed if retried. On /v1/validate/vat this is returned only when we have nothing useful to say at all, see VAT validation during a VIES outage. |
INTERNAL_ERROR | 500 | An unexpected error occurred. If this persists, please contact support. |
VAT validation during a VIES outage
VIES is a gateway to each member state's own VAT service, and those services
go down. When a member state is unavailable, VIES answers with HTTP 200 and no
verdict at all. We never turn that into "valid": false, because a
customer would read it as "this VAT number is invalid" and reject a perfectly
good counterparty.
Instead, /v1/validate/vat/{vatNumber} tells you which of
four situations you are in through a status field. Three of them
are a 200, and only the last is a 502.
| status | HTTP | What it means |
|---|---|---|
validated | 200 | VIES answered, just now or from a cache entry that has not expired. valid is VIES's own verdict. |
stale | 200 | VIES could not be reached, so we are serving a VIES verdict we obtained earlier, past its normal cache lifetime. validatedAt and ageSeconds tell you how old it is. We serve these for up to 30 days, then stop. Treat it as evidence, not as a live confirmation. |
malformed | 200 | VIES could not be reached, but the number fails the Dutch elfproef, so it cannot be a real Dutch VAT number. valid is false and you can act on it. |
| (none) | 502 | UPSTREAM_ERROR. No cached verdict within 30 days and the checksum cannot settle it, so we have nothing. Retry later. This is not a verdict on the number. |
Every response also carries checksumValid, the result of the Dutch
elfproef over the nine digit block. It is true when the number is
well formed, which is not the same as valid: the check rules out
typos and transposed digits, and a random nine digit number still passes about
one time in eleven. It says nothing about whether the number is registered or
active. Only VIES answers that. For a non Dutch number it is null,
because every member state runs its own scheme.
{
"vatNumber": "NL123456789B01",
"valid": true,
"name": "Acme B.V.",
"address": "Herengracht 100, 1015 AA Amsterdam",
"validatedAt": "2026-08-14T09:12:00Z",
"status": "stale",
"ageSeconds": 786240,
"checksumValid": true,
"unavailableReason": "MS_UNAVAILABLE"
}
The same status, checksumValid and
ageSeconds fields appear on the vat object of a company
lookup, plus a fourth value, unvalidated, for a derived VAT candidate
that VIES has never confirmed. There valid is null.
Handling errors
We recommend checking the HTTP status code first, then parsing the error body for more details:
const res = await fetch("https://api.kvkbase.nl/v1/lookup/12345678", {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
if (!res.ok) {
const { error } = await res.json();
switch (error.code) {
case "NOT_FOUND":
console.log("Company not found");
break;
case "RATE_LIMIT_EXCEEDED":
// Wait and retry
const retryAfter = res.headers.get("Retry-After");
break;
case "QUOTA_EXCEEDED":
console.log("Monthly quota reached");
break;
default:
console.error("API error:", error.message);
}
} Rate limit headers
Every response includes rate limit information in the headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for your plan |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the rate limit window resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |