PHP ships with SoapClient, which is enough to query VIES, the European Commission's VAT validation service. Here is working code and the traps to avoid.
<?php
function checkVat(string $raw): array {
$s = strtoupper(preg_replace('/[\s.\-]/', '', $raw));
$country = substr($s, 0, 2) === 'GR' ? 'EL' : substr($s, 0, 2);
$number = substr($s, 2);
$client = new SoapClient(
'https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl',
['connection_timeout' => 10, 'exceptions' => true]
);
try {
$r = $client->checkVat(['countryCode' => $country, 'vatNumber' => $number]);
} catch (SoapFault $f) {
// $f->faultstring: MS_MAX_CONCURRENT_REQ, MS_UNAVAILABLE, INVALID_INPUT...
return ['error' => $f->faultstring];
}
return ['valid' => $r->valid, 'name' => $r->name ?? null, 'address' => $r->address ?? null];
}
soap.wsdl_cache_enabled is on. Keep it on, or reuse the client.MS_MAX_CONCURRENT_REQ and MS_UNAVAILABLE mean a member state is busy or down. Do not refuse the order; validate again later. Details in this guide.Run the check when the customer enters the number, store the result and the date on the order or customer record, and cache it for 24 hours. In WooCommerce, hook woocommerce_after_checkout_validation; in Laravel, a custom validation rule that reads from cache before calling VIES.
$ch = curl_init('https://vatline.p.rapidapi.com/v1/check');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json',
'X-RapidAPI-Key: ' . $key,
'X-RapidAPI-Host: vatline.p.rapidapi.com'],
CURLOPT_POSTFIELDS => json_encode(['vat_number' => 'FR40303265045']),
]);
$result = json_decode(curl_exec($ch), true);
Flat JSON, cached, with a retryable flag on every error.
Skip the plumbing. Vatline wraps VIES in a REST/JSON API with caching, retry hints and per-country circuit breakers. Free plan: 300 checks a month, no card. Read the docs or try a number now.