VAT numbers
Darvis\UblPeppol\ViesService asks VIES whether a VAT number exists. VIES is the European Commission’s service that forwards the question to the tax office of the member state.
Requirements
ViesService uses PHP’s SoapClient, so it needs the soap extension. Composer does not check this for you. Without the extension the call fails with the PHP error Class "SoapClient" not found, which the service does not catch.
php -m | grep soap
Every call is a live request to ec.europa.eu. The connection timeout is 10 seconds.
Check a number
use Darvis\UblPeppol\ViesService;
$vies = new ViesService();
// Country code and number apart
$result = $vies->checkVat('BE', '0999000228');
// Or the full number; the first two characters are the country code
$result = $vies->checkFullVatNumber('BE0999000228');
Spaces are removed, the country code is made upper case, and a country prefix inside the number (checkVat('BE', 'BE0999000228')) is removed. Greece uses EL in VAT numbers, not GR.
In Laravel you can also resolve it from the container with app(ViesService::class). The package registers no binding for it, so you get a new instance.
The result
Both methods return an array and never throw for an answer from VIES.
| Key | When VIES answered | When the call failed |
|---|---|---|
valid | true or false | false |
name | The registered name, or an empty string | null |
address | The registered address, or an empty string | null |
countryCode | The cleaned country code | The country code as you passed it |
vatNumber | The cleaned number without prefix | The number as you passed it |
fullVatNumber | Country code plus number | null |
checked_at | Y-m-d H:i:s | Y-m-d H:i:s |
error | null | A message, see below |
name and address hold what VIES returned. That can be an empty string while valid is true, so do not depend on them.
checkFullVatNumber() with fewer than three characters returns only valid, error (VAT number too short) and checked_at.
Tell “invalid” from “VIES is down”
valid is false in both cases. The difference is error:
$result = $vies->checkFullVatNumber($vatNumber);
if ($result['valid']) {
// The number exists.
} elseif ($result['error'] === null) {
// VIES answered: this number does not exist.
} else {
// The question was never answered. Treat the number as unknown, and try again later.
logger()->warning('VIES check failed', $result);
}
Never block an invoice or a customer on the third case: the question was not answered, so you know nothing about the number.
error | Meaning |
|---|---|
Invalid VAT number format | VIES refused the input (INVALID_INPUT) |
VIES service temporarily unavailable | SERVICE_UNAVAILABLE |
Member state service unavailable | MS_UNAVAILABLE: the tax office of that country does not answer |
Connection timeout | TIMEOUT |
Server is busy, please try again later | SERVER_BUSY |
Too many concurrent requests | MS_MAX_CONCURRENT_REQ or GLOBAL_MAX_CONCURRENT_REQ |
VIES check failed: ... | Any other SOAP fault, with the original text |
What a valid answer proves
It proves the number is registered for trade within the EU at the moment you ask. It does not prove the number belongs to the company you are invoicing; compare name and address yourself when they are filled.
Use it in a Laravel form
A closure rule is a validation rule written as a function (Laravel docs). This one refuses a number VIES says does not exist, and lets the form through when VIES gave no answer:
// app/Http/Controllers/CustomerController.php
use Darvis\UblPeppol\ViesService;
$request->validate([
'vat_number' => [
'required',
function (string $attribute, mixed $value, \Closure $fail) {
$result = app(ViesService::class)->checkFullVatNumber((string) $value);
if (! $result['valid'] && $result['error'] === null) {
$fail('This VAT number is not known in VIES.');
}
},
],
]);
A request waits for VIES here. Cache the result when you check the same number often:
use Illuminate\Support\Facades\Cache;
$result = Cache::remember('vies:'.$vatNumber, now()->addDay(), function () use ($vatNumber) {
return app(ViesService::class)->checkFullVatNumber($vatNumber);
});
Do not cache a result that has an error; that would keep an outage for a day. Check $result['error'] and call Cache::forget() when it is set.
Check only the format, without VIES
UblValidator::validateVatNumber() checks the prefix and the characters without a network call. See Validation.
Test your code without calling VIES
See Testing.