How to Validate VAT Numbers in TypeScript
This guide covers how to validate EU, UK, Swiss, Liechtenstein, Norwegian, and Australian VAT/GST numbers in TypeScript using the @vatly/node SDK. The SDK provides typed responses, a clean error handling pattern, batch validation, and test mode support. We will also show raw fetch() equivalents so you can see what the SDK abstracts away.
Installation
npm install @vatly/nodeBasic validation
The simplest possible example:
import Vatly from "@vatly/node";
const vatly = new Vatly("vtly_live_your_api_key");
const { data, error } = await vatly.vat.validate({
vatNumber: "DE123456789",
});
if (data?.data.valid) {
console.log(`Company: ${data.data.company?.name}`);
console.log(`Country: ${data.data.countryCode}`);
}The SDK returns { data, error }. Exactly one of these is set, never both. data contains the full validation result. error contains a structured error with code and message. The SDK never throws exceptions for API errors.
Error handling
const { data, error } = await vatly.vat.validate({
vatNumber: "INVALID",
});
if (error) {
switch (error.code) {
case "invalid_vat_format":
// The input is not a valid VAT number format
console.error("Bad format:", error.message);
break;
case "upstream_unavailable":
// VIES or HMRC is down
console.error("Upstream down, try again later");
break;
case "rate_limit_exceeded":
// Monthly quota exceeded
console.error("Quota exceeded");
break;
default:
console.error(`Error: ${error.code} - ${error.message}`);
}
return;
}
// data is guaranteed to be set here
console.log(data.data.valid, data.data.vatNumber);All error codes are documented at docs.vatly.dev with explanations and suggested handling. Every error includes a docs_url field pointing to the specific error page.
Batch validation
import Vatly, { isBatchSuccess } from "@vatly/node";
const vatly = new Vatly("vtly_live_your_api_key");
const { data, error } = await vatly.vat.validateBatch({
vatNumbers: [
"DE123456789",
"FR82542065479",
"GB987654321",
"INVALID123",
],
});
if (error) {
console.error("Batch request failed:", error.message);
return;
}
for (const result of data.data.results) {
if (isBatchSuccess(result)) {
console.log(`${result.data.vatNumber}: ${result.data.valid ? "valid" : "invalid"}`);
} else {
console.log(`${result.meta.vat_number}: error - ${result.error.code}`);
}
}
console.log(`${data.data.summary.succeeded}/${data.data.summary.total} succeeded`);Batch validation accepts up to 50 VAT numbers in a single request. Available on Pro and Business tiers. The isBatchSuccess() type guard narrows the result type so TypeScript knows whether you have data or error on each item. Results are returned in the same order as the input.
Test mode
// Use a test key - no upstream calls, no quota usage
const vatly = new Vatly("vtly_test_your_test_key");
// Magic numbers for deterministic testing
const valid = await vatly.vat.validate({ vatNumber: "DE111111111" }); // Always valid
const invalid = await vatly.vat.validate({ vatNumber: "DE000000000" }); // Always invalid
const down = await vatly.vat.validate({ vatNumber: "DE999999999" }); // Simulates upstream outage
const stale = await vatly.vat.validate({ vatNumber: "DE555555555" }); // Stale cache responseDE111111111 always returns valid with a test company. DE000000000 always returns invalid. DE999999999 simulates an upstream service outage. DE555555555 returns a stale cached response. Test mode is ideal for CI/CD pipelines. You get fast, deterministic responses without hitting VIES or HMRC.
Comparison: SDK vs raw fetch
Here's the same basic validation using raw fetch():
const response = await fetch(
"https://api.vatly.dev/v1/validate?vat_number=DE123456789",
{
headers: {
Authorization: "Bearer vtly_live_your_api_key",
},
}
);
const result = await response.json();
if (response.ok) {
const { data, meta } = result;
console.log(data.valid, data.company?.name);
} else {
const { error, meta } = result;
console.error(error.code, error.message);
}The raw fetch approach works fine, but the SDK adds typed responses, automatic error parsing, batch support with type guards, and a cleaner API. If you prefer minimal dependencies, the REST API is straightforward to call directly.
Or with curl:
curl https://api.vatly.dev/v1/validate?vat_number=DE123456789 \
-H "Authorization: Bearer vtly_live_your_api_key"Framework integration examples
Next.js API route
import Vatly from "@vatly/node";
import { NextRequest, NextResponse } from "next/server";
const vatly = new Vatly(process.env.VATLY_API_KEY!);
export async function GET(request: NextRequest) {
const vatNumber = request.nextUrl.searchParams.get("vat_number");
if (!vatNumber) {
return NextResponse.json(
{ error: "vat_number is required" },
{ status: 400 }
);
}
const { data, error } = await vatly.vat.validate({ vatNumber });
if (error) {
return NextResponse.json({ error }, { status: 422 });
}
return NextResponse.json({ data });
}Express middleware
import Vatly from "@vatly/node";
import type { Request, Response, NextFunction } from "express";
const vatly = new Vatly(process.env.VATLY_API_KEY!);
export async function validateVat(req: Request, res: Response, next: NextFunction) {
const vatNumber = req.body.vat_number;
if (!vatNumber) return next();
const { data, error } = await vatly.vat.validate({ vatNumber });
if (error) {
res.status(422).json({ error: error.message });
return;
}
req.vatValidation = data;
next();
}Hono handler
import { Hono } from "hono";
import Vatly from "@vatly/node";
const app = new Hono();
const vatly = new Vatly(process.env.VATLY_API_KEY!);
app.get("/validate", async (c) => {
const vatNumber = c.req.query("vat_number");
if (!vatNumber) {
return c.json({ error: "vat_number is required" }, 400);
}
const { data, error } = await vatly.vat.validate({ vatNumber });
if (error) {
return c.json({ error }, 422);
}
return c.json({ data });
});Python SDK
Looking for Python? The vatly package on PyPI offers the same features with typed exceptions and async support. Install with pip install vatly.
Get started
The free tier includes 500 validations per month. Install the SDK, grab a test key, and start validating. See the full API reference at docs.vatly.dev.
Frequently asked questions
Does the @vatly/node SDK work with Bun and Deno?
The SDK uses standard fetch under the hood, so it works in any JavaScript runtime that supports fetch, including Node.js 18+, Bun, and Deno. No Node.js-specific APIs are required.
Can I use the Vatly API without the SDK?
Yes. The REST API is straightforward to call with fetch, axios, or any HTTP client. The SDK adds typed responses, automatic error parsing, and batch type guards, but the API works the same way with raw HTTP requests.
How does test mode work?
Use an API key prefixed with vtly_test_ instead of vtly_live_. Test mode uses magic VAT numbers (like DE111111111 for valid, DE000000000 for invalid) to return deterministic responses without hitting VIES or HMRC. No quota is consumed in test mode.
What is the isBatchSuccess type guard?
isBatchSuccess() is a TypeScript type guard exported by @vatly/node. It narrows a batch result item to the success type (with data) or error type (with error). This lets TypeScript statically verify you are accessing the correct fields on each batch item.