Use cases
Where VAT validation fits in your stack
Three places teams validate VAT numbers, with code you can paste. Same endpoint everywhere: one GET, JSON back.
01 · Invoicing
Verify before you issue the invoice
Zero-rated intra-EU invoices need a valid customer VAT number at the time of supply. Check it live at that moment and keep the proof.
fresh=1- skips stored results younger than 24 hours, so the answer is from right now.
requester- with your own VAT number makes VIES return a consultation number: the official proof that you checked.
checked_at- tells you exactly when the verdict was given. Store it next to the invoice.
curl -H "Authorization: Bearer $VAT_API_KEY" \
"https://vatapi.be/api/v1/vat/BE0684579082?fresh=1&requester=$OWN_VAT_NUMBER"
use Illuminate\Support\Facades\Http;
$vatNumber = 'BE0684579082';
$ownVatNumber = config('company.vat_number');
// Throws on 4xx/5xx. On 429, wait the seconds in the Retry-After header.
$result = Http::withToken(config('services.vat_api.key'))
->acceptJson()
->timeout(20)
->get('https://vatapi.be/api/v1/vat/'.rawurlencode($vatNumber), ['fresh' => 1, 'requester' => $ownVatNumber])
->throw()
->json();
// Right before issuing the invoice: a live check, with a VIES consultation number as proof.
$proof = match ($result['valid']) {
true => ['checked_at' => $result['checked_at'], 'consultation_number' => $result['consultation_number']], // keep with the invoice
false => null, // not valid: invoice with VAT, no zero rate
null => null, // no definitive answer right now: retry before issuing
};
const vatNumber = 'BE0684579082';
const ownVatNumber = process.env.OWN_VAT_NUMBER;
const response = await fetch(`https://vatapi.be/api/v1/vat/${encodeURIComponent(vatNumber)}?${new URLSearchParams({ fresh: '1', requester: ownVatNumber })}`, {
headers: { Authorization: `Bearer ${process.env.VAT_API_KEY}`, Accept: 'application/json' },
signal: AbortSignal.timeout(20_000),
});
// On 429, wait the seconds in the Retry-After header.
if (!response.ok) throw new Error(`VAT API error ${response.status}`);
const result = await response.json();
// Right before issuing the invoice: a live check, with a VIES consultation number as proof.
let proof = null;
if (result.valid === true) {
proof = { checkedAt: result.checked_at, consultationNumber: result.consultation_number }; // keep with the invoice
}
// false: not valid, invoice with VAT. null: no definitive answer right now, retry before issuing.
import os
from urllib.parse import quote
import requests
vat_number = "BE0684579082"
own_vat_number = os.environ["OWN_VAT_NUMBER"]
response = requests.get(
f"https://vatapi.be/api/v1/vat/{quote(vat_number, safe='')}",
params={"fresh": 1, "requester": own_vat_number},
headers={"Authorization": f"Bearer {os.environ['VAT_API_KEY']}", "Accept": "application/json"},
timeout=20,
)
response.raise_for_status() # on 429, wait response.headers["Retry-After"] seconds
result = response.json()
# Right before issuing the invoice: a live check, with a VIES consultation number as proof.
proof = None
if result["valid"] is True:
proof = {"checked_at": result["checked_at"], "consultation_number": result["consultation_number"]} # keep with the invoice
# False: not valid, invoice with VAT. None: no definitive answer right now, retry before issuing.
02 · B2B checkout
Reverse charge at checkout, without blocking orders
A business customer in another member state enters their VAT number; a valid number means you can apply the reverse charge. Speed matters here, and so does not failing when VIES has a bad day.
first answer- wins the race, so a slow or busy source does not hold up the checkout.
valid: null- means "could not decide right now", never a guess. Charge VAT or flag the order for review instead of rejecting it.
Retry-After- tells your retry job when to ask again.
curl -H "Authorization: Bearer $VAT_API_KEY" \
"https://vatapi.be/api/v1/vat/NL004495445B01"
use Illuminate\Support\Facades\Http;
$vatNumber = 'NL004495445B01';
// Throws on 4xx/5xx. On 429, wait the seconds in the Retry-After header.
$result = Http::withToken(config('services.vat_api.key'))
->acceptJson()
->timeout(20)
->get('https://vatapi.be/api/v1/vat/'.rawurlencode($vatNumber))
->throw()
->json();
// Reverse charge only for a confirmed valid number from another member state.
$reverseCharge = $result['valid'] === true && $result['country_code'] !== $sellerCountry;
// null = no definitive answer right now: charge VAT now and re-check later, don't block the order.
$needsReview = $result['valid'] === null;
const vatNumber = 'NL004495445B01';
const response = await fetch(`https://vatapi.be/api/v1/vat/${encodeURIComponent(vatNumber)}`, {
headers: { Authorization: `Bearer ${process.env.VAT_API_KEY}`, Accept: 'application/json' },
signal: AbortSignal.timeout(20_000),
});
// On 429, wait the seconds in the Retry-After header.
if (!response.ok) throw new Error(`VAT API error ${response.status}`);
const result = await response.json();
// Reverse charge only for a confirmed valid number from another member state.
const reverseCharge = result.valid === true && result.country_code !== sellerCountry;
// null = no definitive answer right now: charge VAT now and re-check later, don't block the order.
const needsReview = result.valid === null;
import os
from urllib.parse import quote
import requests
vat_number = "NL004495445B01"
response = requests.get(
f"https://vatapi.be/api/v1/vat/{quote(vat_number, safe='')}",
headers={"Authorization": f"Bearer {os.environ['VAT_API_KEY']}", "Accept": "application/json"},
timeout=20,
)
response.raise_for_status() # on 429, wait response.headers["Retry-After"] seconds
result = response.json()
# Reverse charge only for a confirmed valid number from another member state.
reverse_charge = result["valid"] is True and result["country_code"] != seller_country
# None = no definitive answer right now: charge VAT now and re-check later, don't block the order.
needs_review = result["valid"] is None
03 · CRM / ERP onboarding
Clean customer records from the first keystroke
Validate when a customer or supplier is created, normalise the number, and prefill the registered name and address instead of retyping them.
vat_number- comes back normalised (BE 0684.579.082 becomes BE0684579082), ready to store.
name / address- are filled in where the member state discloses them.
stale: true- on an older stored result is still an instant answer, refreshed in the background, so bulk re-checks stay fast.
curl -H "Authorization: Bearer $VAT_API_KEY" \
"https://vatapi.be/api/v1/vat/DE811907980"
use Illuminate\Support\Facades\Http;
$vatNumber = 'DE811907980';
// Throws on 4xx/5xx. On 429, wait the seconds in the Retry-After header.
$result = Http::withToken(config('services.vat_api.key'))
->acceptJson()
->timeout(20)
->get('https://vatapi.be/api/v1/vat/'.rawurlencode($vatNumber))
->throw()
->json();
// false = not registered: ask for a correction. null = no answer right now: accept and re-check later.
$customer = [
'vat_number' => $result['vat_number'], // normalised, e.g. BE0684579082
'name' => $name ?: $result['name'], // prefill from the register
'address' => $address ?: $result['address'],
'vat_valid' => $result['valid'],
'vat_checked_at' => $result['checked_at'],
];
const vatNumber = 'DE811907980';
const response = await fetch(`https://vatapi.be/api/v1/vat/${encodeURIComponent(vatNumber)}`, {
headers: { Authorization: `Bearer ${process.env.VAT_API_KEY}`, Accept: 'application/json' },
signal: AbortSignal.timeout(20_000),
});
// On 429, wait the seconds in the Retry-After header.
if (!response.ok) throw new Error(`VAT API error ${response.status}`);
const result = await response.json();
// false = not registered: ask for a correction. null = no answer right now: accept and re-check later.
const customer = {
vatNumber: result.vat_number, // normalised, e.g. BE0684579082
name: name || result.name, // prefill from the register
address: address || result.address,
vatValid: result.valid,
vatCheckedAt: result.checked_at,
};
import os
from urllib.parse import quote
import requests
vat_number = "DE811907980"
response = requests.get(
f"https://vatapi.be/api/v1/vat/{quote(vat_number, safe='')}",
headers={"Authorization": f"Bearer {os.environ['VAT_API_KEY']}", "Accept": "application/json"},
timeout=20,
)
response.raise_for_status() # on 429, wait response.headers["Retry-After"] seconds
result = response.json()
# False = not registered: ask for a correction. None = no answer right now: accept and re-check later.
customer = {
"vat_number": result["vat_number"], # normalised, e.g. BE0684579082
"name": name or result["name"], # prefill from the register
"address": address or result["address"],
"vat_valid": result["valid"],
"vat_checked_at": result["checked_at"],
}
Good to know for every integration
Keep your key server-side
Call the API from your backend. Never ship the key in browser or app code.
Use a generous timeout
Most answers are quick, but a live check can take longer when VIES is struggling. 20 seconds is a safe client timeout.
Respect the limits
60 requests per minute and 5,000 per day. A 429 comes with Retry-After.
This page explains how to use the API, it is not tax advice. Check the VAT rules that apply to your business with your accountant.
Start validating in a minute
Free API key by email. No credit card, no password.