This guide gets you from zero to a working VAT number validation in Node.js as fast as possible. No new npm packages required on Node.js 18+ — built-in fetch handles the HTTP call. If you need the full production-ready implementation with caching, a service class, and a complete test suite, see the Node.js EU VAT validation tutorial. This guide is deliberately minimal: get the first call working, understand the response, then add the right error handling.
Note
You need a TaxID API key to follow this guide. Get one free at taxid.dev/signup — the free tier includes 100 validations/month, no credit card required.
Step 1: Add Your API Key
Store your key as an environment variable. Never hardcode it in source files or commit it to version control — treat it like a database password.
TAXID_API_KEY=vat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx// Node.js 20.6+ supports --env-file natively:
// node --env-file=.env server.js
// Older versions: use dotenv
// npm install dotenv
// require('dotenv').config();Step 2: Your First Validation Call
The TaxID API uses a simple REST pattern: `GET /api/v1/validate/:country/:vat`. The country code is the two-letter ISO prefix that appears at the start of every VAT number — for `DE123456789`, the country is `DE`.
const BASE = 'https://taxid.dev/api/v1';
async function validateVat(vatNumber) {
const country = vatNumber.slice(0, 2).toUpperCase();
const vat = vatNumber.replace(/\s/g, '').toUpperCase();
const res = await fetch(`${BASE}/validate/${country}/${vat}`, {
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`API error ${res.status}`);
return res.json();
}
// Try it:
const result = await validateVat('DE123456789');
console.log(result);Step 3: Understanding the Response
Every validation response has the same shape. The `status` field is the one that drives all your application logic — not `valid`, which is just a boolean shorthand for `status === 'active'`.
| Field | Type | Description |
|---|---|---|
| valid | boolean | true only when status is 'active' |
| status | string | One of: active, inactive, format_invalid, service_unavailable |
| company_name | string | null | Registered company name from the tax authority |
| address | string | null | Registered address from the tax authority |
| country_code | string | Two-letter country code (e.g. DE, FR, GB) |
| cached | boolean | true if result was served from TaxID cache |
| request_id | string | Unique ID for this validation — log it for audit |
Step 4: Handle All Four Status Codes
There are exactly four status values. Each requires different application logic. The most common mistake is treating `service_unavailable` the same as `inactive` — do not zero-rate based on an unavailable response.
async function handleVatCheck(vatNumber) {
let data;
try {
data = await validateVat(vatNumber);
} catch {
// Network error or timeout — treat same as service_unavailable
return { outcome: 'unavailable' };
}
switch (data.status) {
case 'active':
return {
outcome: 'valid',
companyName: data.company_name,
address: data.address,
};
case 'inactive':
return { outcome: 'invalid', reason: 'not_registered' };
case 'format_invalid':
return { outcome: 'invalid', reason: 'bad_format' };
case 'service_unavailable':
// VIES is down — charge standard VAT, re-validate later
return { outcome: 'unavailable' };
default:
return { outcome: 'unavailable' };
}
}Warning
When `status` is `service_unavailable`, never silently apply zero-rate VAT. The safest approach is to charge standard VAT and issue a credit note once VIES recovers and confirms the number is valid.
Adding a Validation Endpoint in Express
In a typical Express.js application, expose VAT validation as a POST endpoint your frontend can call. Keep the API key on the server — never pass it to the browser.
import express from 'express';
const router = express.Router();
router.post('/validate-vat', async (req, res) => {
const { vatNumber } = req.body;
if (!vatNumber || typeof vatNumber !== 'string') {
return res.status(400).json({ error: 'vat_number_required' });
}
const result = await handleVatCheck(vatNumber.trim());
if (result.outcome === 'invalid') {
return res.status(422).json({
error: 'invalid_vat',
reason: result.reason,
});
}
return res.json({
valid: result.outcome === 'valid',
companyName: result.companyName ?? null,
unavailable: result.outcome === 'unavailable',
});
});
export default router;TypeScript Version
For TypeScript projects, add types to the response and the handler return value to get full IDE autocompletion and type safety throughout your application.
type VatStatus = 'active' | 'inactive' | 'format_invalid' | 'service_unavailable';
interface VatResponse {
valid: boolean;
status: VatStatus;
company_name: string | null;
address: string | null;
country_code: string;
cached: boolean;
request_id: string;
}
export async function validateVat(vatNumber: string): Promise<VatResponse> {
const country = vatNumber.slice(0, 2).toUpperCase();
const vat = vatNumber.replace(/\s/g, '').toUpperCase();
const res = await fetch(
`https://taxid.dev/api/v1/validate/${country}/${vat}`,
{
headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY!}` },
signal: AbortSignal.timeout(5000),
}
);
if (!res.ok) throw new Error(`TaxID API ${res.status}`);
return res.json() as Promise<VatResponse>;
}Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.