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 response format
Every error response has this structure:
{
"error": {
"code": "NOT_FOUND",
"message": "Company not found for KVK number 99999999"
}
} Error code reference
All possible error codes, their HTTP status, and common causes:
NOT_FOUND 404 No company found for the given identifier.
INVALID_KVK_NUMBER 400 The provided KVK number is not valid.
INVALID_VAT_NUMBER 400 The provided VAT number format is not recognized.
INVALID_QUERY 400 The search parameters were rejected.
INVALID_REQUEST 400 The request is missing required parameters or has invalid values.
UNAUTHORIZED 401 Missing or invalid API key.
RATE_LIMIT_EXCEEDED 429 Too many requests in the current time window.
QUOTA_EXCEEDED 429 Monthly token allowance exhausted.
UPSTREAM_ERROR 502 An upstream service is temporarily unavailable.
INTERNAL_ERROR 500 An unexpected server error occurred.
Handling errors
We recommend checking the HTTP status code first, then parsing the error body for the specific error code:
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":
const retryAfter = res.headers.get("Retry-After");
console.log(`Rate limited. Retry after ${retryAfter}s`);
break;
case "QUOTA_EXCEEDED":
console.log("Monthly token quota reached");
break;
case "UNAUTHORIZED":
console.log("Invalid API key");
break;
default:
console.error("API error:", error.message);
}
} Rate limit headers
Every API response includes headers with rate limit and token usage information. Use these to monitor your usage and avoid hitting limits.
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests per minute for your plan | 60 |
X-RateLimit-Remaining | Requests remaining in the current window | 58 |
X-RateLimit-Reset | Unix timestamp when the rate limit window resets | 1710676800 |
X-Tokens-Remaining | Tokens remaining for the current billing cycle | 847 |
Retry-After | Seconds to wait before retrying (only on 429 responses) | 12 |
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.
Retry strategy
For production applications, we recommend implementing exponential backoff:
async function kvkLookup(kvkNumber, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(
`https://api.kvkbase.nl/v1/lookup/${kvkNumber}`,
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
if (res.ok) return res.json();
if (res.status === 429) {
const wait = parseInt(res.headers.get("Retry-After") || "5");
await new Promise(r => setTimeout(r, wait * 1000));
continue;
}
if (res.status === 502) {
// Upstream error -- wait and retry
await new Promise(r => setTimeout(r, (attempt + 1) * 2000));
continue;
}
// Non-retryable error
const { error } = await res.json();
throw new Error(error.message);
}
throw new Error("Max retries exceeded");
}