Skip to content
tutorial

VAT number validation API: verify EU tax IDs in one POST

| 5 min read
EU flags in front of the European Parliament
Photo by Olga Subach on Unsplash

You sell software to businesses in Europe. EU tax law requires you to validate the buyer's VAT ID before applying reverse charge (0% VAT). The official VIES service has unreliable uptime and returns SOAP XML. You need a faster, RESTful alternative.

The botoi /v1/validate/vat endpoint validates VAT numbers for all 27 EU member states and the UK. Send a POST, get a JSON response with validity, country code, and the formatted number. No XML parsing. No WSDL files. No timeouts from overloaded government servers.

The API call

Send a VAT number with the two-letter country prefix:

curl -X POST https://api.botoi.com/v1/validate/vat \
  -H "Content-Type: application/json" \
  -d '{
    "vat_number": "DE123456789"
  }'

Response:

{
  "success": true,
  "data": {
    "valid": true,
    "country_code": "DE",
    "country": "Germany",
    "formatted": "DE123456789"
  },
  "meta": {
    "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "duration": 3
  }
}

The valid field is true when the number matches the expected format for its country. The country_code and country fields tell you which jurisdiction the number belongs to.

When a number fails validation

If the number doesn't match the expected pattern, valid comes back false. The response still includes the detected country:

curl -X POST https://api.botoi.com/v1/validate/vat \
  -H "Content-Type: application/json" \
  -d '{
    "vat_number": "DE12345"
  }'
{
  "success": true,
  "data": {
    "valid": false,
    "country_code": "DE",
    "country": "Germany",
    "formatted": "DE12345"
  },
  "meta": {
    "requestId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
    "duration": 2
  }
}

German VAT numbers require exactly 9 digits after the DE prefix. This one has 5. Your frontend can display the country name and expected format to help the user correct their input.

Stripe checkout integration

Before creating a Stripe checkout session, validate the buyer's VAT ID. If it's valid, set the customer to tax_exempt: "reverse" so Stripe charges 0% VAT. If it's invalid, reject the form and ask the buyer to correct it.

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function validateVatBeforeCheckout(vatNumber: string, customerId: string) {
  // Step 1: Validate the VAT format
  const res = await fetch("https://api.botoi.com/v1/validate/vat", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer YOUR_API_KEY",
    },
    body: JSON.stringify({ vat_number: vatNumber }),
  });

  const result = await res.json();

  if (!result.data.valid) {
    throw new Error(`Invalid VAT number: ${vatNumber}`);
  }

  // Step 2: Apply tax exemption in Stripe
  await stripe.customers.update(customerId, {
    tax_exempt: "reverse",
    tax_ids: [{ type: "eu_vat", value: vatNumber }],
  } as any);

  // Step 3: Create checkout session with 0% VAT
  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: "subscription",
    line_items: [{ price: "price_1ABC123", quantity: 1 }],
    success_url: "https://app.example.com/billing?status=success",
    cancel_url: "https://app.example.com/billing?status=cancelled",
  });

  return session.url;
}

This prevents two common problems: charging VAT to a business that should be exempt (creating a refund headache), and granting a tax exemption to someone with a fake VAT number (creating an audit problem).

Invoice generation with conditional VAT

When generating an invoice, you need to decide the VAT rate. The rules:

  • Same-country B2B sale: charge your domestic VAT rate.
  • Cross-border B2B sale with a valid VAT ID: apply reverse charge (0%).
  • Invalid or missing VAT ID: charge the buyer's local rate.

The country_code from the API response drives this logic:

interface Invoice {
  customer_name: string;
  vat_number: string;
  country_code: string;
  subtotal: number;
  vat_rate: number;
  vat_amount: number;
  total: number;
}

const EU_VAT_RATES: Record<string, number> = {
  DE: 19, FR: 20, NL: 21, ES: 21, IT: 22, AT: 20,
  BE: 21, PT: 23, IE: 23, PL: 23, SE: 25, DK: 25,
  FI: 24, EL: 24, CZ: 21, HU: 27, RO: 19, BG: 20,
  HR: 25, SK: 20, SI: 22, LT: 21, LV: 21, EE: 22,
  CY: 19, MT: 18, LU: 17,
};

async function generateInvoice(
  customerName: string,
  vatNumber: string,
  subtotal: number,
  sellerCountry: string
): Promise<Invoice> {
  const res = await fetch("https://api.botoi.com/v1/validate/vat", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer YOUR_API_KEY",
    },
    body: JSON.stringify({ vat_number: vatNumber }),
  });

  const result = await res.json();
  const buyerCountry = result.data.country_code;
  const validVat = result.data.valid;

  // B2B reverse charge: 0% VAT if buyer is in a different EU country with a valid VAT ID
  let vatRate = 0;
  if (!validVat || buyerCountry === sellerCountry) {
    vatRate = EU_VAT_RATES[buyerCountry] || 0;
  }

  const vatAmount = subtotal * (vatRate / 100);

  return {
    customer_name: customerName,
    vat_number: vatNumber,
    country_code: buyerCountry,
    subtotal,
    vat_rate: vatRate,
    vat_amount: vatAmount,
    total: subtotal + vatAmount,
  };
}

SaaS B2B signup form validation

Add a VAT number field to your signup form and validate it on submit. If valid, auto-fill the country dropdown. If invalid, show an error before the form reaches your backend.

async function handleSignup(event: Event) {
  event.preventDefault();

  const form = event.target as HTMLFormElement;
  const vatInput = form.querySelector<HTMLInputElement>("#vat-number");
  const errorEl = form.querySelector<HTMLElement>("#vat-error");
  const vatNumber = vatInput?.value.trim() || "";

  if (!vatNumber) {
    // VAT is optional for B2C customers
    submitForm(form);
    return;
  }

  const res = await fetch("https://api.botoi.com/v1/validate/vat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ vat_number: vatNumber }),
  });

  const result = await res.json();

  if (!result.data.valid) {
    if (errorEl) {
      errorEl.textContent = "Invalid VAT number. Check the format and country prefix.";
      errorEl.classList.remove("hidden");
    }
    vatInput?.focus();
    return;
  }

  // Auto-fill the country dropdown from the VAT prefix
  const countrySelect = form.querySelector<HTMLSelectElement>("#country");
  if (countrySelect) {
    countrySelect.value = result.data.country_code;
  }

  submitForm(form);
}

This keeps bad data out of your billing system. It also saves your finance team from manually verifying VAT numbers after the customer has already signed up and started using the product.

VAT format by country

Each EU country has a different format for VAT identification numbers. The API validates against all of these patterns:

Country        | Prefix | Format              | Example
───────────────|────────|─────────────────────|──────────────────
Germany        | DE     | DE + 9 digits       | DE123456789
France         | FR     | FR + 2 chars + 9 digits | FR12345678901
Netherlands    | NL     | NL + 9 digits + B + 2 digits | NL123456789B01
Spain          | ES     | ES + 1 char + 7 digits + 1 char | ESX1234567A
Italy          | IT     | IT + 11 digits      | IT12345678901
United Kingdom | GB     | GB + 9 or 12 digits | GB123456789
Austria        | AT     | ATU + 8 digits      | ATU12345678
Belgium        | BE     | BE0 + 9 digits      | BE0123456789
Poland         | PL     | PL + 10 digits      | PL1234567890
Sweden         | SE     | SE + 12 digits      | SE123456789012

The full list covers all 27 EU member states plus the UK. Formats range from 8 digits (Denmark, Luxembourg) to 12 digits (Sweden). Some countries include letters in the body of the number (France, Spain, Ireland).

EU flags waving in front of the European Parliament building
Photo by Alexandre Lallemand on Unsplash

Format validation vs VIES lookup

This API validates the structure of a VAT number. It confirms the prefix, length, and character pattern match the rules for that country. It does not query the EU Commission's VIES database to confirm the number is actively registered.

For most checkout flows, format validation is the right first step. It catches typos, missing prefixes, and made-up numbers instantly, without depending on an external service that's frequently down. If you also need registration status, call VIES after the format check passes. This two-step approach reduces VIES calls by 15-30% (all the invalid formats never hit the slow service).

Key points

- POST /v1/validate/vat checks format and structure for all 27 EU member states plus GB
- Send { "vat_number": "DE123456789" } and get back valid (boolean), country_code, country name, and formatted number
- Response time is under 5ms; no external VIES dependency, no SOAP XML
- Free tier: 5 requests per minute, no API key needed
- Use the country_code field to determine VAT rate and reverse-charge eligibility
- The API never stores or logs submitted VAT numbers

The free tier covers development and low-volume production use. For high-traffic checkout flows, add your API key in the Authorization: Bearer header. Check the API docs for the full endpoint reference and the interactive playground to test VAT numbers in your browser.

Frequently asked questions

Which countries does the VAT validation API support?
All 27 EU member states, plus the UK (GB prefix). Each country has its own format pattern. The API detects the country from the two-letter prefix and validates against the correct regex for that jurisdiction.
Does this replace the VIES SOAP service?
It replaces the format-validation part. VIES confirms whether a specific number is registered with a national tax authority. This API validates structure and format instantly, without depending on VIES uptime. Use both together: format-check first, then VIES lookup for registered status.
Is the VAT number stored after validation?
No. The number is processed in memory and discarded after the response. Nothing is written to disk or any external system.
Can I validate UK VAT numbers after Brexit?
UK VAT numbers (GB prefix) follow a known pattern and can be structurally validated. They are no longer part of the EU VIES system, but format checking still works.
What happens if I omit the country prefix?
The API requires the two-letter country prefix (e.g., "DE", "FR", "NL"). Without it, the endpoint returns an error explaining the country code is missing or unsupported.

Try this API

VAT Validation API — interactive playground and code examples

More tutorial posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.