Iurii RoguliaIurii Rogulia
AboutServicesPricingProjectsStackReviewsPhrasesBlog
Contact
Iurii ships.

Iurii Rogulia, IT partner for business & fractional CTO. Professionally building software since 2001.

Think of a number
PricingQuality checklistPrivacy PolicyCookie Policy

Business

TMI Iurii Rogulia
VAT ID: FI29845875
DUNS: 368664211
Lappeenranta, Finland 🇫🇮

[email protected]
Back to projects

Iurii Handles: EU VAT Rates for WooCommerce — WordPress Plugin

August 5, 2026

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.

Live demo Source code

Stack

PHPWordPressWooCommerceJavaScript

Services

WordPress.orgEuropean Commission TEDBvatnode API

Topics

WordPress PluginE-commerceB2BTax/VATCheckoutOpen SourceAutomation

Key Results

  • EU tax rates stay correct on their own — a rate change lands in the store within 24 hours
  • Verified EU business buyers pay net, with the reverse charge applied before the order is placed
  • VAT number, VIES verdict, registered company name and consultation number stored on the order as audit evidence
  • Checkout is never blocked: no key, spent quota or VIES outage still lets the order through with VAT charged
EU VAT Rates for WooCommerce — WordPress Plugin

The Business Problem

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.

The Solution

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.

Results

MetricValue
Releasev1.1.1, 5 August 2026
Codebase~1,740 lines PHP across 15 classes, ~230 lines JS/CSS
RequirementsPHP 8.1+, WordPress 6.3+, WooCommerce 8.0–11.0
CheckoutsBoth — classic shortcode and cart/checkout blocks
CountriesEU-27 + XI for VIES, 45 for rate lookup
API cost1 request per VAT number, cached 24 hours
CompatibilityHPOS and cart/checkout blocks declared
LicenseGPL-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.

Under the Hood

For the technically curious, here is how the core pieces are built.

Making VAT disappear before the order is placed

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 );
Block checkout — the VAT number field in the contact section

Offline first, fail-open always

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.

Order screen — VAT number, verification status, reverse charge and registered company name

The bug that made every synced rate invisible

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.

Sync that cannot make things worse

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.

Iurii RoguliaAvailable

Need something similar?

I build custom solutions — from APIs to full products. Let's talk about your project.

View all projects

Related projects

eu-vat-rates-data — Free & Open-Source EU VAT Rates Dataset
eu-vat-rates-data — Free & Open-Source EU VAT Rates Dataset
February 25, 2026
eu-vat-rates-data — Free & Open-Source EU VAT Rates Dataset

Free, open-source EU VAT rates for all 27 member states + UK. Published as native packages for npm, PyPI, Packagist, Go, and RubyGems.

Stack

TypeScriptPythonPHPGoRuby

Services

GitHub ActionsnpmPyPIPackagistRubyGems

Topics

Open SourceTax/VATAutomation
n8n Community Node — EU VAT Validation in Workflows
n8n Community Node — EU VAT Validation in Workflows
August 6, 2026
n8n Community Node — EU VAT Validation in Workflows

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

Stack

TypeScriptNode.jsn8n

Services

npmGitHub Actionsvatnode API

Topics

Workflow AutomationNo-CodeIntegrationsDeveloper ToolsOpen SourceTax/VAT

Related posts

Publishing One Package to Five Registries with GitHub Actions
March 27, 2026· 10 min
Publishing One Package to Five Registries with GitHub Actions

How to publish one dataset to npm, PyPI, Go Module, RubyGems, and Packagist automatically with GitHub Actions — architecture, versioning, and per-ecosystem

Stack

TypeScriptPythonPHPGoRuby

Services

GitHub ActionsnpmPyPIPackagistRubyGems

Topics

Open SourceTax/VATAutomation
Gated B2B Pricing in Next.js: Hiding Prices Behind Per-Customer Access
July 24, 2026· 9 min
Gated B2B Pricing in Next.js: Hiding Prices Behind Per-Customer Access

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

Stack

Next.jsReactTypeScript

Databases

PostgreSQL

Topics

B2BE-commerceAuthSSRArchitectureSecurity