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.checkId) // "019d2a89-..." — store for audit trailTypeScript response types
Add these interfaces to get full type safety on the API response:
interface CountryVat {
vatName: string // e.g. "Umsatzsteuer", "TVA", "ALV"
vatAbbr: string // e.g. "MwSt", "TVA", "VAT"
currency: string // ISO 4217, e.g. "EUR", "PLN", "HUF"
standardRate: number
reducedRates: number[]
superReducedRate: number | null
parkingRate: number | null
ratesUpdatedAt: string
}
interface VatResult {
valid: boolean
vatId: string
countryCode: string
countryName: string
companyName: string | null
companyAddress: string | null
source: 'VIES' | 'BZST'
checkId: string // UUID — store on your invoice
verifiedAt: string // ISO 8601 timestamp
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
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, error: 'VIES_UNAVAILABLE' }
}
if (res.status === 400) {
// Invalid VAT number format
return { valid: false, checkId: null, error: 'INVALID_FORMAT' }
}
const data: VatResult = await res.json()
return { valid: data.valid, checkId: data.checkId }
} catch {
// Network error — treat same as VIES unavailable
return { valid: null, checkId: 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.checkId 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,
vatCheckId: vatResult?.checkId ?? null, // audit trail
vatVerifiedAt: vatResult?.verifiedAt ?? null,
}
return Response.json({ invoice })
}
function getLocalVatRate(currency: string): number {
// Your local VAT rate logic
return 0.23
}Related guides
Ready to add VAT validation to your Node.js app?
Free plan: 20 requests/month, no credit card required. Up and running in under 5 minutes.