vatly.dev is becoming avatcado.com. api.vatly.dev shuts down on 1 July 2026, so switch your API base URL to api.avatcado.com today to keep everything working. Your API keys stay the same.

How to Validate VAT Numbers in TypeScript

By Avatcado Team

This guide covers how to validate EU, UK, Swiss, Liechtenstein, Norwegian, and Australian VAT/GST numbers in TypeScript using the @avatcado/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 @avatcado/node

Basic validation

The simplest possible example:

import Avatcado from "@avatcado/node";

const avatcado = new Avatcado("avat_live_your_api_key");

const { data, error } = await avatcado.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 avatcado.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.avatcado.com with explanations and suggested handling. Every error includes a docs_url field pointing to the specific error page.

Batch validation

import Avatcado, { isBatchSuccess } from "@avatcado/node";

const avatcado = new Avatcado("avat_live_your_api_key");

const { data, error } = await avatcado.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 avatcado = new Avatcado("avat_test_your_test_key");

// Magic numbers for deterministic testing
const valid = await avatcado.vat.validate({ vatNumber: "DE111111111" });     // Always valid
const invalid = await avatcado.vat.validate({ vatNumber: "DE000000000" });   // Always invalid
const down = await avatcado.vat.validate({ vatNumber: "DE999999999" });      // Simulates upstream outage
const stale = await avatcado.vat.validate({ vatNumber: "DE555555555" });     // Stale cache response

DE111111111 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.avatcado.com/v1/validate?vat_number=DE123456789",
  {
    headers: {
      Authorization: "Bearer avat_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.avatcado.com/v1/validate?vat_number=DE123456789 \
  -H "Authorization: Bearer avat_live_your_api_key"

Framework integration examples

Next.js API route

import Avatcado from "@avatcado/node";
import { NextRequest, NextResponse } from "next/server";

const avatcado = new Avatcado(process.env.AVATCADO_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 avatcado.vat.validate({ vatNumber });

  if (error) {
    return NextResponse.json({ error }, { status: 422 });
  }

  return NextResponse.json({ data });
}

Express middleware

import Avatcado from "@avatcado/node";
import type { Request, Response, NextFunction } from "express";

const avatcado = new Avatcado(process.env.AVATCADO_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 avatcado.vat.validate({ vatNumber });

  if (error) {
    res.status(422).json({ error: error.message });
    return;
  }

  req.vatValidation = data;
  next();
}

Hono handler

import { Hono } from "hono";
import Avatcado from "@avatcado/node";

const app = new Hono();
const avatcado = new Avatcado(process.env.AVATCADO_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 avatcado.vat.validate({ vatNumber });

  if (error) {
    return c.json({ error }, 422);
  }

  return c.json({ data });
});

Python SDK

Looking for Python? The avatcado package on PyPI offers the same features with typed exceptions and async support. Install with pip install avatcado.

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.avatcado.com.

Get your API key →

Frequently asked questions

Does the @avatcado/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 Avatcado 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 avat_test_ instead of avat_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 @avatcado/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.