Error Handling
vatnode uses standard HTTP status codes and returns detailed error information in JSON format.
Error Response Format
All errors follow this structure:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"requestId": "req_abc123",
"details": {}
}
}| Field | Type | Description |
|---|---|---|
| code | string | Machine-readable error code |
| message | string | Human-readable description |
| requestId | string | Unique request ID for debugging |
| details | object | Additional context (optional) |
HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request – Invalid input |
| 401 | Unauthorized – Invalid or missing API key |
| 403 | Forbidden – Insufficient permissions, or VIES reports the VAT ID or your IP as blocked (VIES_ERROR) |
| 404 | Not Found – Resource doesn't exist |
| 422 | Unprocessable Entity – configured requester VAT is invalid in VIES |
| 429 | Too Many Requests – Monthly quota exhausted |
| 500 | Internal Server Error |
| 502 | Bad Gateway – upstream error (VIES), or the check could not be recorded |
| 503 | Service Unavailable |
| 504 | Gateway Timeout – VIES timeout |
Error Codes
INVALID_REQUEST
The request body, query or path parameters failed structural validation – a missing or mistyped field, or a value outside the allowed range (for example, a bulk batch over the size limit). The message names the first offending field. Fix the request before retrying.
{
"error": {
"code": "INVALID_REQUEST",
"message": "vatIds: Required",
"requestId": "req_abc123"
}
}INVALID_FORMAT
The VAT number format is invalid – either the country code is not a recognised EU member state, the number doesn't match the expected pattern, or VIES itself rejected the input.
{
"error": {
"code": "INVALID_FORMAT",
"message": "Invalid VAT ID format for DE",
"requestId": "req_abc123"
}
}INVALID_REQUESTER
Returned only when a requester VAT is configured in dashboard Account details (under “VAT for VIES consultation numbers”) and VIES rejected that requester VAT with INVALID_INPUT (deregistered, typo, or otherwise unknown to VIES).
{
"error": {
"code": "INVALID_REQUESTER",
"message": "Your requester VAT number is invalid in VIES. Fix or clear it in dashboard Account details. Clearing the requester disables consultation numbers but allows national fallback when VIES is down.",
"viesCode": "INVALID_INPUT",
"requestId": "req_abc123"
}
}How to handle:
- Open dashboard Account details and either correct the VAT or clear the field.
- Clearing the requester disables consultation numbers on every response but re-enables national-registry fallback when VIES is temporarily down.
- vatnode deliberately does not silently retry the call without the requester: a successful response without a consultation number would strip the audit evidence the caller opted into.
UNAUTHORIZED
The request has no Authorization header, or the header does not contain an API key. Send Authorization: Bearer {your_api_key}.
{
"error": {
"code": "UNAUTHORIZED",
"message": "API key required. Include Authorization: Bearer {your_api_key}",
"requestId": "req_abc123"
}
}INVALID_API_KEY
The API key was sent but is not recognised or has been revoked. Check the key in the dashboard and create a new one if needed. Retrying with the same key will not help.
{
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid or revoked API key",
"requestId": "req_abc123"
}
}RATE_LIMITED
Returned when your plan's monthly request quota is exhausted:
- Free plan: the 100 requests/month limit was reached, so further live calls are rejected until the next billing period.
- Starter, Pro, and Enterprise: never quota-limited – requests past the included quota are billed at a pay-as-you-go rate and the service continues uninterrupted, so these plans do not return this error under normal use.
{
"error": {
"code": "RATE_LIMITED",
"message": "Monthly quota exceeded. Limit: 100, used: 100. Upgrade your plan or wait for the next billing period.",
"requestId": "req_abc123"
}
}This is the only condition that produces a 429. vatnode does not apply a per-second or per-minute request rate limit – throttling is by monthly quota alone. Every response carries RateLimit-* headers reporting quota, not a short-window limit; see Rate Limits for the full reference.
How to handle:
- Upgrade to Starter or Pro to continue past the free monthly quota.
- Cache validation results to avoid repeating the same checks.
- Wait for the next billing period, when the quota window resets.
VIES_UNAVAILABLE
The VIES service is temporarily unavailable. Behaviour depends on whether a requester is set:
- No requester: vatnode first tries the national registry fallback for supported countries. This error is returned only when both VIES and the fallback fail, or when no fallback exists for the country.
- Requester set: national fallback is deliberately skipped (a result without a consultation number would strip the audit evidence you opted into), so the error is returned as soon as VIES fails. Retry when VIES recovers, or temporarily clear the requester in dashboard Account details to accept results without consultation numbers.
{
"error": {
"code": "VIES_UNAVAILABLE",
"message": "VIES service is temporarily unavailable",
"viesCode": "SERVICE_UNAVAILABLE",
"requestId": "req_abc123"
}
}VIES_ERROR
VIES returned an error that is not a verdict on the number. It arrives as 502 for VIES protocol faults, and as 403 when VIES reports the VAT ID or your IP as blocked (VAT_BLOCKED / IP_BLOCKED). A retry of a 403 repeats the block, so don't loop on it – proceed on your default and review it instead.
{
"error": {
"code": "VIES_ERROR",
"message": "An error occurred while validating VAT",
"viesCode": "IP_BLOCKED",
"requestId": "req_abc123"
}
}VIES-specific codes returned in the viesCode field:
| viesCode | Description |
|---|---|
| MS_UNAVAILABLE | Member state's national service is down or not responding |
| MS_MAX_CONCURRENT_REQ | Member state's service is busy; max concurrent requests exceeded |
| GLOBAL_MAX_CONCURRENT_REQ | VIES global concurrent request limit exceeded |
| SERVICE_UNAVAILABLE | Central VIES service is unavailable |
| TIMEOUT | VIES request timed out |
| VAT_BLOCKED | The requested VAT number is blocked in VIES (VIES_ERROR, HTTP 403) |
| IP_BLOCKED | The requesting IP address is blocked by VIES (VIES_ERROR, HTTP 403) |
UPSTREAM_TIMEOUT
VIES did not answer inside the timeout, and no national-registry fallback took over – the country has none, its fallback failed too, or a requester VAT is configured and fallback is deliberately skipped (a result without a consultation number would strip the audit evidence you opted into). Distinct from VIES_UNAVAILABLE, which means VIES said it was down; a timeout means it said nothing at all. No verdict was delivered, so nothing was counted against your quota – retry it.
{
"error": {
"code": "UPSTREAM_TIMEOUT",
"message": "VIES service timed out",
"viesCode": "TIMEOUT",
"requestId": "req_abc123"
}
}In a bulk job this is a per-position error like any other: the position carries valid: null and an error object, the job still reaches finished, and you resubmit the timed-out VAT IDs as a new job.
AUDIT_WRITE_FAILED
The check itself ran, but vatnode could not store its record – so it withheld the verdict instead of handing you a checkId for a check it can no longer produce. A checkId is a receipt for a stored record, and that is the whole point of it. Nothing was counted against your quota; retry the request. In a bulk job it is a per-position error, never a batch-level one: POST /v1/vat/bulk validates nothing during the request, so there is no batch write to fail. A position whose record could not be stored gets no checkId, is not billed, and is safe to resubmit as part of a new job; the rest of the batch is unaffected.
{
"error": {
"code": "AUDIT_WRITE_FAILED",
"message": "The VAT check ran, but its audit record could not be stored, so no checkId was issued and no verdict is returned. This request was not counted against your quota. Retry it.",
"requestId": "req_abc123"
}
}NOT_FOUND
The requested resource does not exist: an unknown path, a country code with no rate data on /v1/rates/{countryCode}, or a bulk job id that does not belong to your account.
{
"error": {
"code": "NOT_FOUND",
"message": "VAT rates not found for country: ZZ",
"requestId": "req_abc123"
}
}ALREADY_EXISTS
The resource you are trying to create already exists (e.g., duplicate API key label or webhook URL).
{
"error": {
"code": "ALREADY_EXISTS",
"message": "A resource with this identifier already exists.",
"requestId": "req_abc123"
}
}CREATION_FAILED
An internal error occurred while creating a resource. Retrying the request is safe.
{
"error": {
"code": "CREATION_FAILED",
"message": "Failed to create resource. Please try again.",
"requestId": "req_abc123"
}
}Best Practices
1. Always Check HTTP Status
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 400:
console.error('Invalid request:', error.error.message);
break;
case 429:
console.error('Monthly quota exhausted:', error.error.message);
break;
case 503:
console.error('Service unavailable, retrying...');
break;
default:
console.error('Error:', error.error.message);
}
}2. Implement Retry Logic
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response.json();
if (response.status === 429 || response.status >= 500) {
// No Retry-After header — back off exponentially
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
continue;
}
// Client error, don't retry
const error = await response.json();
throw new Error(error.error.message);
} catch (error) {
if (attempt === maxRetries) throw error;
}
}
}3. Log Request IDs
Always log the requestId from error responses. This helps us investigate issues:
try {
const result = await validateVat('IE6388047V');
} catch (error) {
console.error('Validation failed:', {
message: error.message,
requestId: error.requestId, // Include this in support tickets
});
}4. Handle VIES Downtime Gracefully
VIES has scheduled maintenance and occasional outages. Consider showing a user-friendly message:
async function validateVatWithFallback(vatId) {
try {
return await validateVat(vatId);
} catch (error) {
if (error.code === 'VIES_UNAVAILABLE') {
return {
valid: null, // Unknown
vatId,
message: 'VAT validation temporarily unavailable. Will verify later.',
pendingVerification: true,
};
}
throw error;
}
}Support
If you encounter persistent errors:
- Check the vatnode status page for known issues
- Review your API key and monthly usage in the Dashboard
- Contact support with your
requestIdfor investigation