Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.
WordPress plugin that keeps WooCommerce EU tax rates current from the European Commission and applies the B2B reverse charge from a VIES-verified VAT number at checkout, powered by my own vatnode API.
Key Results

A WooCommerce store that sells to businesses across the EU has two tax problems, and both are usually discovered by an accountant rather than by the owner.
The first is the tax table. EU standard rates move more often than anyone expects: Estonia raised its rate twice in recent years, Finland went to 25.5%. WooCommerce charges whatever was typed into the tax table on the day the shop was set up, and nothing in WordPress ever tells you that a number has gone stale.
The second is the reverse charge. When a verified business in another EU country buys, VAT should not be charged at all. Doing that by hand means checking the number on the European Commission's VIES site, then editing the order afterwards — which is why most small stores either charge VAT to everyone or switch tax off and hope.
Both are exactly the kind of recurring, rule-based work a plugin should absorb, and WooCommerce runs a large share of European B2B storefronts. The plugin directory is where those owners go looking — and I already had the pieces to answer both questions, built for vatnode, my EU VAT validation service. What was missing was the WordPress side.
I built a WordPress plugin that does both jobs with no configuration to get started.
Rates come from the European Commission's TEDB via my open eu-vat-rates-data dataset, written straight into the WooCommerce tax table and re-checked daily. Only the EU-27 plus Northern Ireland are written — a Norwegian or Swiss rate in that table would make WooCommerce tax an export that is not owed.
At checkout a VAT number field appears on both the classic shortcode checkout and the newer React block checkout. Without an account the number is format-checked against its country pattern and stored on the order. Add a vatnode API key and the number is verified live against VIES: when it is valid and the buyer is in another EU country, VAT comes off the total and the reverse charge applies — while the shopper is still on the page, not after the order is placed.
The free/paid split follows the work. Everything answerable offline — format, country eligibility, whether the sale is even cross-border — is free and needs no account; only a number that could actually trigger a reverse charge costs a verification request. A plugin nobody can try is a plugin nobody installs.
| Metric | Value |
|---|---|
| Release | v1.1.1, 5 August 2026 |
| Codebase | ~1,740 lines PHP across 15 classes, ~230 lines JS/CSS |
| Requirements | PHP 8.1+, WordPress 6.3+, WooCommerce 8.0–11.0 |
| Checkouts | Both — classic shortcode and cart/checkout blocks |
| Countries | EU-27 + XI for VIES, 45 for rate lookup |
| API cost | 1 request per VAT number, cached 24 hours |
| Compatibility | HPOS and cart/checkout blocks declared |
| License | GPL-2.0-or-later, public repository |
Submitted to the WordPress.org plugin directory; review is pending. It installs today from the repository or as a zip, and is documented at vatnode.dev/woocommerce.
The plugin is where the vatnode stack meets a shop owner who will never write a line of code — alongside the SaaS API for developers, the open dataset on five package registries and the MCP server for AI assistants. WordPress asks for different engineering than any of them: no build step on the server, PHP 8.1 against a decade of legacy WooCommerce behaviour, two entirely separate checkout architectures to support at once, and repairs that have to fix installs already running in production.
For the technically curious, here is how the core pieces are built.
The block checkout is a React app talking to the WooCommerce Store API. The field itself is straightforward — the Additional Checkout Fields API, registered in the contact location rather than address, so the value travels with the customer request rather than the billing block:
woocommerce_register_additional_checkout_field( [
'id' => 'vatnode/vat-number',
'label' => __( 'VAT number', 'vatnode-eu-vat-rates' ),
'location' => 'contact',
'type' => 'text',
'sanitize_callback' => [ __CLASS__, 'sanitize' ],
'validate_callback' => [ __CLASS__, 'validate' ],
] );The problem is timing. WooCommerce sends contact fields to the Store API when the order is placed, not while the shopper types — so a valid VAT number would remove VAT only after checkout. A shopper who sees VAT on the total abandons the cart. So the field's value is pushed to cart/update-customer on blur, and the returned cart is fed back into the store:
fetcher({
path: "/wc/store/v1/cart/update-customer",
method: "POST",
data: { additional_fields: { "vatnode/vat-number": value } },
}).then(receiveCart);The second trap is on the server. The Store API stamps is_vat_exempt onto the order before the plugin's hook runs, and WC_Abstract_Order::calculate_taxes() reads that meta rather than the customer object — so setting the exemption on the customer and recalculating puts the tax straight back on. Both have to be set, in that order:
EUVATR_Validator::apply_exemption( $evaluation['exempt'] );
$order->update_meta_data( 'is_vat_exempt', $evaluation['exempt'] ? 'yes' : 'no' );
$order->calculate_taxes();
$order->calculate_totals( false );
The validator is a ladder of cheap checks before an expensive one. Empty field, wrong format for the country, VAT country not matching the billing country, same country as the store — all answered locally, for free, and only what is left can trigger a reverse charge and justify an API call.
Everything below that ladder is fail-open. No key, spent quota, VIES down, network error: the status becomes unverified, VAT is charged as usual, the reason is written to the order notes, and the order goes through. An upstream service I do not control must never be able to block someone's checkout — the worst case is a business buyer paying VAT they can reclaim, which is recoverable. A blocked checkout is a lost sale, which is not.
Answers are cached in the site for 24 hours, keyed on the number itself, so entering a VAT number costs one quota request no matter how many times the totals refresh between the field and the confirmed order — including the consultation number that goes on the order as evidence.

Version 1.0.0 wrote each synced rate with a matching row in woocommerce_tax_rate_locations, which looks like the obvious way to say "this rate is for Germany". It is not. The country already lives in tax_rate_country; the locations table exists for postcode and city restrictions, and a row there makes WC_Tax::find_rates() require a location match that a country-wide rate can never satisfy.
The result is the worst failure mode a tax plugin can have: the settings screen showed 27 correct rates, and WooCommerce applied none of them. No error, no warning — just no tax on any order.
The fix had to repair existing installs, not only new ones, so the sync starts by dropping the rows it should never have written:
DELETE l FROM {locations} l
INNER JOIN {rates} r ON r.tax_rate_id = l.tax_rate_id
WHERE l.location_type = 'country'
AND r.tax_rate_name = 'VAT'
AND r.tax_rate_class = ''The same release does the reverse for a second mistake — versions up to 1.1.0 wrote a rate for every country in the dataset, including non-EU ones, which made WooCommerce add Norwegian VAT to an export. Those rows are deleted on the next sync. Both repairs are scoped to rates this plugin manages: a VAT name in the standard class. Anything a merchant added by hand is left alone.
Rate writing is an upsert by country code — update if a managed rate exists, insert if not. Rates that disappear from the dataset are left in place rather than deleted, because the store owner may depend on them and a missing rate is a silent undercharge.
The daily job runs on WP-Cron, with a single event scheduled ten seconds after activation so the first sync happens without anyone clicking anything. The download is a static JSON file from GitHub — no authentication, no personal data, and nothing to be rate-limited by. If it fails, the previous rates stay exactly where they are and the settings screen shows the reason.
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.
Community node that puts VIES VAT validation and EU rate lookups inside n8n, so an onboarding or invoicing workflow can decide on a real VAT number instead of
How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist automatically with GitHub Actions — architecture, versioning, and per-ecosystem
Gated B2B pricing in Next.js: per-account price lists, server-side access control so prices never leak to crawlers, and a deal-record data model where every