EU VAT Number Validation in Node.js
A complete guide to validating EU VAT numbers in Node.js and TypeScript using the vatnode REST API. Covers basic validation, TypeScript types, checkout flow integration, and VIES error handling.
Why validate EU VAT numbers in Node.js?
If your Node.js application sells to EU businesses (B2B), you need to validate customer VAT numbers before issuing invoices. A valid VAT number in VIES (the EU's official VAT database) lets you apply the reverse charge mechanism — charging zero VAT on cross-border B2B supplies. Applying reverse charge without VIES confirmation is a compliance risk.
The official VIES API uses SOAP — a legacy XML protocol that requires separate libraries and significant boilerplate. The vatnode API wraps VIES in a clean REST interface. One fetch() call, structured JSON response.
Quick start
No package installation needed. The vatnode API works with the built-in fetch in Node.js 18+ (or any HTTP library):
const VATNODE_API_KEY = process.env.VATNODE_API_KEY!
async function validateVatNumber(vatId: string) {
const response = await fetch(
`https://api.vatnode.dev/v1/vat/${encodeURIComponent(vatId)}`,
{
headers: { Authorization: `Bearer ${VATNODE_API_KEY}` },
}
)
if (!response.ok) {
const err = await response.json()
throw new Error(err.error?.code ?? `HTTP ${response.status}`)
}
return await response.json()
}
// Usage
const result = await validateVatNumber('IE6388047V')
console.log(result.valid) // true
console.log(result.companyName) // "GOOGLE IRELAND LIMITED"
console.log(result.consultationNumber) // "WAPIAAAAZ27qPadm" — EU Commission audit token, store on invoice
console.log(result.checkId) // "019d2a89-..." — vatnode audit trail UUIDTypeScript response types
Add these interfaces to get full type safety on the API response:
interface CountryVat {
vatName: string // e.g. "Mehrwertsteuer", "Taxe sur la Valeur Ajoutée", "Arvonlisävero"
vatAbbr: string // e.g. "MwSt", "TVA", "ALV"
currency: string // ISO 4217, e.g. "EUR", "PLN", "HUF"
standardRate: number
reducedRates: number[]
superReducedRate: number | null
parkingRate: number | null
countryVatUpdatedAt: string
}
interface VatResult {
valid: boolean
vatId: string
countryCode: string
countryName: string
companyName: string | null
companyAddress: string | null
source: string // 'VIES' or national registry code (e.g. 'BZST', 'MF_PL', 'SIREN_FR')
checkId: string // UUID — vatnode audit trail identifier
verifiedAt: string // ISO 8601 timestamp of the check
consultationNumber: string | null // EU Commission audit token — store on your invoice
countryVat: CountryVat
}
interface VatError {
error: {
code: string // e.g. "VIES_UNAVAILABLE", "INVALID_FORMAT"
message: string
}
}Handle VIES errors gracefully
VIES national nodes go offline for maintenance. Your application must handle VIES_UNAVAILABLE without blocking the user:
async function validateVatSafe(vatId: string): Promise<{
valid: boolean | null // null = VIES unavailable, retry later
checkId: string | null
consultationNumber: string | null
error?: string
}> {
try {
const res = await fetch(
`https://api.vatnode.dev/v1/vat/${encodeURIComponent(vatId)}`,
{ headers: { Authorization: `Bearer ${VATNODE_API_KEY}` } }
)
if (res.status === 503) {
// VIES temporarily unavailable — queue for retry
return { valid: null, checkId: null, consultationNumber: null, error: 'VIES_UNAVAILABLE' }
}
if (res.status === 400) {
// Invalid VAT number format
return { valid: false, checkId: null, consultationNumber: null, error: 'INVALID_FORMAT' }
}
const data: VatResult = await res.json()
return { valid: data.valid, checkId: data.checkId, consultationNumber: data.consultationNumber }
} catch {
// Network error — treat same as VIES unavailable
return { valid: null, checkId: null, consultationNumber: null, error: 'NETWORK_ERROR' }
}
}
// In your checkout handler
const vat = await validateVatSafe(customerVatId)
if (vat.error === 'VIES_UNAVAILABLE') {
// Queue for async retry — do not block checkout
await queue.add('validate-vat', { vatId: customerVatId, customerId })
// Apply standard VAT in the meantime
} else if (vat.valid) {
// Apply reverse charge, store vat.consultationNumber on the invoice
} else {
// Invalid VAT — charge standard VAT rate
}Full checkout flow example
A complete example for validating a VAT number during B2B checkout and deciding whether to apply reverse charge:
// POST /api/checkout — Next.js Route Handler or Express endpoint
export async function POST(req: Request) {
const { vatId, amount, currency } = await req.json()
let vatResult = null
let applyReverseCharge = false
if (vatId) {
const res = await fetch(
`https://api.vatnode.dev/v1/vat/${encodeURIComponent(vatId)}`,
{ headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` } }
)
if (res.ok) {
vatResult = await res.json() as VatResult
applyReverseCharge = vatResult.valid
}
// If 503, treat as unverified — charge VAT
}
const invoice = {
lineItems: [{ amount, currency }],
vatRate: applyReverseCharge ? 0 : getLocalVatRate(currency),
reverseChargeNote: applyReverseCharge
? 'Reverse charge — VAT to be accounted for by the recipient'
: undefined,
vatConsultationNumber: vatResult?.consultationNumber ?? null, // EU Commission audit token
vatCheckId: vatResult?.checkId ?? null,
vatVerifiedAt: vatResult?.verifiedAt ?? null,
}
return Response.json({ invoice })
}
function getLocalVatRate(currency: string): number {
// Your local VAT rate logic
return 0.23
}Recommended audit record structure
For each B2B invoice where reverse charge is applied, persist at minimum these fields from the vatnode response alongside the invoice record:
{
"invoiceId": "inv_2024_001",
"vatNumber": "IE6388047V",
"consultationNumber": "WAPIAAAAZ27qPadm", // EU Commission audit token
"validatedAt": "2026-03-26T14:25:57.209Z",
"countryCode": "IE"
}The consultationNumber is the EU Commission-issued reference that tax authorities can independently verify. Returned when your requester VAT is configured and VIES answers directly — see the audit trail guide for the full storage pattern.
Related guides
Ready to add VAT validation to your Node.js app?
Free plan: 100 requests/month, no credit card required. Up and running in under 5 minutes.