WooCommerce has built-in EU VAT number handling (via the EU VAT Number plugin or WooCommerce's own tax settings), but it only validates format — it checks whether the string looks like a German or French VAT number, without ever calling VIES. This means a customer can enter a syntactically valid but completely unregistered VAT number and receive zero-rate treatment. For EU businesses, this creates VAT liability. This guide shows how to add real VIES validation via PHP hooks, with correct error handling and audit records.
Why WooCommerce's built-in VAT check is not enough
| Feature | WooCommerce built-in | With TaxID API hook |
|---|---|---|
| Format validation | Yes (regex only) | Yes (regex + VIES confirmation) |
| VIES real-time check | No | Yes — every checkout |
| Company name verified | No | Yes — returned in API response |
| service_unavailable handling | N/A | Yes — fail open, flag for retry |
| Subscription re-validation | No | Yes — woocommerce_scheduled_subscription_payment |
| Audit trail (order meta) | No | Yes — request_id + company name stored |
Warning
The WooCommerce EU VAT Number plugin and WooCommerce's built-in EU tax settings perform format validation only — they do not call VIES. A customer who enters DE123456789 (a format-valid number that does not exist in VIES) will pass the built-in check and receive reverse-charge treatment. You are then liable for the VAT you did not charge.
The fix is to add a VIES validation call inside the woocommerce_checkout_process action hook, which fires before the order is created. If the validation fails, you add a checkout error notice and halt the order. The customer sees a clear message and can correct their VAT number.
Adding real-time VIES validation via a hook
<?php
// Add to functions.php or a custom plugin
add_action( 'woocommerce_checkout_process', 'taxid_validate_vat_number' );
function taxid_validate_vat_number() {
$vat_number = sanitize_text_field( $_POST['billing_vat_number'] ?? '' );
if ( empty( $vat_number ) ) return; // No VAT number provided — B2C treatment
// Extract country code from VAT prefix
$country_code = strtoupper( substr( $vat_number, 0, 2 ) );
$response = wp_remote_get(
"https://www.taxid.dev/api/v1/validate/{$country_code}/{$vat_number}",
[
'headers' => [
'Authorization' => 'Bearer ' . TAXID_API_KEY,
],
'timeout' => 10,
]
);
if ( is_wp_error( $response ) ) {
// Network error — allow through, log for follow-up
error_log( 'TaxID API error: ' . $response->get_error_message() );
return;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$status = $body['status'] ?? 'error';
if ( $status === 'service_unavailable' ) {
// VIES is down — allow through, schedule re-validation
WC()->session->set( 'vat_pending_revalidation', $vat_number );
return;
}
if ( $status !== 'active' ) {
wc_add_notice(
__(
'The VAT number you entered could not be verified in the EU VIES system. '
. 'Please check the number and try again, or proceed without a VAT number.',
'woocommerce'
),
'error'
);
}
}Storing the validation record in order meta
Once the order is created, store the validation result in order meta. You will need this for audit purposes — specifically the company name (to verify it matches what the customer declared), and a request ID to prove that validation was performed.
<?php
add_action( 'woocommerce_checkout_order_created', 'taxid_store_vat_validation', 10, 1 );
function taxid_store_vat_validation( $order ) {
$vat_number = sanitize_text_field( $_POST['billing_vat_number'] ?? '' );
if ( empty( $vat_number ) ) return;
$country_code = strtoupper( substr( $vat_number, 0, 2 ) );
$response = wp_remote_get(
"https://www.taxid.dev/api/v1/validate/{$country_code}/{$vat_number}",
[ 'headers' => [ 'Authorization' => 'Bearer ' . TAXID_API_KEY ], 'timeout' => 10 ]
);
if ( is_wp_error( $response ) ) return;
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$order->update_meta_data( '_taxid_vat_status', $body['status'] ?? '' );
$order->update_meta_data( '_taxid_vat_company_name', $body['company_name'] ?? '' );
$order->update_meta_data( '_taxid_request_id', $body['request_id'] ?? '' );
$order->update_meta_data( '_taxid_validated_at', current_time( 'mysql', true ) );
$order->save();
}Re-validation for WooCommerce Subscriptions
For subscription-based WooCommerce stores using the WooCommerce Subscriptions plugin, you need to re-validate the VAT number before each renewal payment — a business can deregister between renewals, and applying reverse charge based on a stale validation creates liability.
<?php
add_action( 'woocommerce_scheduled_subscription_payment', 'taxid_revalidate_subscription_vat', 5, 2 );
function taxid_revalidate_subscription_vat( $amount, $order ) {
$subscription = wcs_get_subscription( $order );
if ( ! $subscription ) return;
$vat_number = $subscription->get_meta( '_billing_vat_number' );
if ( empty( $vat_number ) ) return;
$country_code = strtoupper( substr( $vat_number, 0, 2 ) );
$response = wp_remote_get(
"https://www.taxid.dev/api/v1/validate/{$country_code}/{$vat_number}",
[ 'headers' => [ 'Authorization' => 'Bearer ' . TAXID_API_KEY ], 'timeout' => 10 ]
);
if ( is_wp_error( $response ) ) return;
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$status = $body['status'] ?? '';
if ( $status !== 'active' && $status !== 'service_unavailable' ) {
// VAT number is no longer valid — switch to standard rate
// Add a note to the subscription for the finance team
$subscription->add_order_note(
sprintf(
__( 'VAT re-validation failed (status: %s). Review tax treatment before next renewal.', 'woocommerce' ),
esc_html( $status )
)
);
// Optionally: update tax_exempt flag, trigger finance notification
}
// Store result on the renewal order
$order->update_meta_data( '_taxid_revalidation_status', $status );
$order->update_meta_data( '_taxid_revalidation_request_id', $body['request_id'] ?? '' );
$order->save();
}Testing your integration
- →Valid active VAT number: use DE114111111 (a real test number from the German tax authority) — should pass and return status: active
- →Invalid VAT number: use DE000000000 — should fail validation and show the checkout error notice
- →Format-invalid number: use DE12 (too short) — should trigger format_invalid before hitting VIES
- →Service unavailable scenario: temporarily use an incorrect API key to simulate an error response — verify that the order is allowed through (fail open)
Related resources
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.