DEV Community

VatPulse
VatPulse

Posted on Originally published at api.vatpulse.workers.dev

VAT Number Check API: Validate EU VAT Numbers from Your Code

If you charge EU business customers, you need to check their VAT number before applying the reverse charge. The official tool is VIES, but its web form isn't an integration, and its API has quirks that take a while to learn the hard way.

This guide shows how to add a reliable VAT number check to your app with a single HTTP call.

What a "VAT number check" actually answers

A VAT check answers one question: is this number currently registered for intra-EU trade? Depending on the country, you may also get the registered company name and address.

Things to know up front:

  • The prefix matters. EU VAT numbers start with a 2-letter code: DE, FR, NL... Greece uses EL, not GR. Northern Ireland uses XI.
  • Name/address availability varies. Some member states (Germany and Spain, for example) don't return them through VIES. That's not an error.
  • The UK left VIES. Since 2021, GB numbers are checked with HMRC's own API, which needs separate OAuth credentials.
  • Formats differ wildly. ATU12345678, NL123456789B01, FRXX123456789, IE1234567WA. A local format check catches typos before any network call.

The one-call version

The examples use VatPulse, a VIES wrapper I built; most wrappers work similarly. You send the number as the user typed it (spaces, dots and dashes are fine):

curl -X POST https://vatpulse1.p.rapidapi.com/validate \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: vatpulse1.p.rapidapi.com" \
  -H "Content-Type: application/json" \
  -d '{"vat": "IE 6388047 V"}'
Enter fullscreen mode Exit fullscreen mode
{
  "vat": {
    "input": "IE 6388047 V",
    "valid": true,
    "vat_number": "IE6388047V",
    "country_code": "IE",
    "country_name": "Ireland",
    "name": "GOOGLE IRELAND LIMITED",
    "address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
    "source": "vies",
    "cached": false,
    "stale": false,
    "checked_at": "2026-10-17T22:44:26.350Z",
    "error": null,
    "warning": null
  },
  "billable": true,
  "usage": { "plan": "rapidapi", "used": 1, "limit": null, "remaining": null, "reset_at": "2026-11-01T00:00:00.000Z", "note": "Quota and billing for RapidAPI subscriptions are managed by RapidAPI." }
}
Enter fullscreen mode Exit fullscreen mode

source tells you who confirmed it (vies, hmrc, or syntax for local format rejections), and checked_at gives you a timestamp for your audit trail. On RapidAPI, usage.limit is null because RapidAPI tracks your plan quota.

JavaScript

async function checkVat(vat) {
  const res = await fetch("https://vatpulse1.p.rapidapi.com/validate", {
    method: "POST",
    headers: {
      "X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
      "X-RapidAPI-Host": "vatpulse1.p.rapidapi.com",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ vat }),
  });
  const data = await res.json();
  return data.vat; // { valid: true | false | null, name, address, error, ... }
}
Enter fullscreen mode Exit fullscreen mode

Python

import os, requests

def check_vat(vat: str) -> dict:
    r = requests.post(
        "https://vatpulse1.p.rapidapi.com/validate",
        headers={
            "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
            "X-RapidAPI-Host": "vatpulse1.p.rapidapi.com",
        },
        json={"vat": vat},
        timeout=15,
    )
    return r.json()["vat"]
Enter fullscreen mode Exit fullscreen mode

Handle three outcomes, not two

The single most important design decision: valid is three-valued.

valid Meaning Typical action
true Registered Apply reverse charge, show company name
false Not registered or malformed Ask the user to correct it, or charge VAT
null Couldn't be determined right now Accept provisionally and re-check later, or ask user to retry

When valid is false or null, the error.code tells you why:

  • invalid_format: doesn't match that country's format (caught locally, free)
  • invalid_country: unknown prefix
  • member_state_unavailable: that country's system is down
  • service_unavailable: VIES/HMRC is down
  • upstream_rate_limited: VIES is throttling

And when VIES is down but the number was confirmed recently, you get valid: true with stale: true and a warning, so outages don't block your customers. (More on that in my post on VIES downtime.)

UK VAT numbers

GB numbers aren't in VIES any more. HMRC has its own "Check a UK VAT number" API (v2.0): GET https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup/{vrn} with Accept: application/vnd.hmrc.2.0+json and an OAuth 2.0 client-credentials token (scope read:vat) from a registered HMRC Developer Hub app. A 404 means the number isn't registered.

VatPulse has this integration built, but it isn't switched on yet, so GB numbers currently return service_unavailable. Northern Ireland XI numbers work today because they go through VIES.

Caching and cost

Successful results are cached for 24 hours. Repeat checks of the same number (signup, then checkout, then invoice) return cached: true and don't count against the quota. Errors don't count either; only fresh, definitive answers (valid or invalid) count.

Your plan quota is shown in your RapidAPI dashboard, and you can also query usage from the API:

curl https://vatpulse1.p.rapidapi.com/usage \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: vatpulse1.p.rapidapi.com"
Enter fullscreen mode Exit fullscreen mode

Try it

Disclosure: I built VatPulse, the API used in the examples above. The patterns work just as well if you build them yourself.

Top comments (0)