Iurii RoguliaIurii Rogulia
AboutAI EngineeringServicesProjectsAI SkillsStackTestimonialsPhrasesBlog
Contact
Iurii ships.

Iurii Rogulia, senior full-stack software engineer. Professionally building software since 2001.

Think of a numberThink of a birthday
Hourly ratesChecklistsPrivacy PolicyCookie Policy

Business

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

  1. Home
  2. /
  3. Blog
  4. /
  5. WooCommerce VAT Plugin Fail-Open When the Validation API Is Down

Iurii Degrades: WooCommerce VAT Plugin Fail-Open When the Validation API Is Down

A WooCommerce tax plugin API call can fail in two very different ways: no answer, or a definitive no. Only one of them should let the order through. Here is where my plugin draws the line.

November 6, 2026· 9 min read

A WooCommerce tax plugin should fail open when its API gives no answer, not when the answer is no. Where my VAT plugin draws that line, and what it costs.

Stack

PHPWordPressWooCommerce

Services

vatnode APIVIES

Topics

API DesignResilienceE-commerceTax/VATCheckout
WooCommerce VAT Plugin Fail-Open When the Validation API Is Down

‘Fail open’ is shorthand that hides the interesting part. When a WooCommerce plugin calls an external API during checkout and the API is down, the plugin does not simply let everything through. In my EU VAT plugin for WooCommerce it fails open only when it cannot get an answer. A definitive ‘no’ (a malformed number, a country mismatch, a number VIES says is not registered) still blocks the order.

That distinction is the whole design. Below is the decision ladder as the code runs it, what the API has to guarantee for the ladder to be safe, and what fail-open costs the buyer and the store. It also covers a regression in an early release that taught me what ‘cannot check’ must never be allowed to mean.

The decision ladder

Every VAT number typed at checkout goes through Vatnode_Validator::evaluate(). The order of the checks matters, so here it is in the order the code runs them:

StepConditionStatusBlocks checkout?
1Field is emptyemptyNo
2Rate dataset unavailableunverifiedNo
3Store is not in an EU/VIES-eligible countrynot_eligibleNo
4Wrong format for the country, or prefix not VIES-eligibleformat_invalidYes
5VAT number country differs from billing countrycountry_mismatchYes
6VAT number country is the store’s own countrydomesticNo (VAT charged)
7Live validation not active (no API key, or switched off)unverifiedNo
8aAPI answers INVALID_FORMATformat_invalidYes
8bAPI call fails any other wayunverifiedNo (VAT charged)
8cAPI answers 200 with valid: falseinvalidYes
8dAPI answers 200 with valid: truevalidNo (reverse charge)

Steps 4 and 5 run before any network call. A number with the wrong shape for its country, or one that belongs to a different country than the billing address, is rejected from the plugin’s own data. That is a definitive answer and it does not depend on anyone being up.

Step 8b is the fail-open branch, and it is deliberately wide. In the code it is a single condition: if the call did not produce a usable answer, for any reason other than INVALID_FORMAT, the result is unverified, no exemption is applied, VAT is charged as usual and the order goes through. A missing key, a transport error, a 401 or 403, a 429 and any other non-200 all land there. I did not try to sort failures into ‘retry’ and ‘give up’ at this layer. The plugin has exactly one question at that point (did I get a verdict?), and the answer is yes or no.

The ladder, reduced to the one decision that matters:

// Simplified from Vatnode_Validator::evaluate()
$response = Vatnode_Api::validate( $vat_number );
 
if ( ! $response['ok'] ) {
    if ( $response['error_code'] === 'INVALID_FORMAT' ) {
        return result( FORMAT_INVALID, blocking: true );   // definitive no
    }
    return result( UNVERIFIED, blocking: false );           // no answer: fail open
}
 
return $response['valid']
    ? result( VALID, exempt: true )                         // reverse charge
    : result( INVALID, blocking: true );                    // definitive no

The API contract that makes this safe

The ladder only works if ‘no answer’ and ‘a no’ arrive in different shapes. If the API reported an upstream outage as valid: false, step 8c would treat an outage as a definitive rejection and block real buyers.

This is not a hypothetical. The raw VIES service can answer valid: false for numbers that are fine when a member state’s own registry is down; I covered that in EU VAT Number Validation in Next.js and will not repeat it here. The consequence for an integration is that someone has to translate. In my EU VAT validation API, a member-state registry outage or a concurrency limit comes back as HTTP 503, a timeout as 504 (if the client waits long enough), and network or parse failures as 502. The plugin trusts that contract: anything that is not a 200 is ‘no answer’, and only a 200 carries a verdict.

If you are designing the API side of a pair like this, that is the part to get right first. A fail-open client is only as safe as the API’s refusal to turn ‘I could not find out’ into ‘no’.

What gets cached, and what does not

Verdicts are cached for 24 hours in a WordPress transient keyed on the normalised VAT number. That covers successful 200 answers, including valid: false, and INVALID_FORMAT. Failures are not cached.

A verdict is a fact about the number and stays true for a day. A failure is a fact about the moment and says nothing about the number, so caching it would extend an outage past its end. The price is that during an outage every evaluation makes a fresh outbound request.

The cost of failing open

Failing open is a trade, and the cost lands on a specific person.

If a legitimate business buyer with a valid VAT number checks out while validation is unavailable, they are charged VAT. They may be able to recover it, but they paid it first. The sale the store would have lost by blocking the checkout is kept. I think that is the right side of the trade for this domain: the alternative is a lost order caused by someone else’s infrastructure, and the cost of the error is bounded and recoverable.

Two things make that cost larger than it needs to be, and I would rather state them than hide them:

  • The plugin does not re-check. I searched the code for any path that revisits an unverified order later (retry, recheck, refund) and found none. Recovering the VAT is a manual job outside the plugin.
  • The order record is thin on cause. Each evaluated order gets meta fields (number, status, country, a reverse-charge flag, a timestamp) and one order note: the VAT number plus the status label, for example ‘Not verified – VAT charged’. The underlying cause (quota exceeded, API unreachable, key rejected) is not in the note. It shows in the key status on the plugin’s settings screen. If you want to know why last Tuesday’s orders were unverified, the order alone will not tell you.

The wait-time trade-off

Fail-open protects the order. It does not protect the shopper’s time. This part I derived from reading the code, not from measuring it.

On the classic checkout, the place-order path calls evaluate() three times: while preparing posted data, during checkout validation, and when creating the order. The AJAX totals refresh calls it as well. The request timeout is 8 seconds. When the API is slow or unreachable at the transport level and nothing is cached, each of those calls can wait up to the timeout, so a single submit can in principle wait through three of them in a row. Fast refusals such as a 401, 403 or 429 do not have this problem. A slow upstream does: my API is willing to wait on VIES longer than the plugin is willing to wait on my API, so a slow registry reaches the plugin as a transport timeout rather than as a tidy 503 or 504. From the code, that is the likeliest way for this path to go wrong.

I have not measured this against a real outage, so treat the number as a ceiling derived from the code, not an observed delay. It also shows why ‘fail open’ is not a complete resilience strategy. It answers ‘does the order survive?’ and says nothing about ‘how long did the buyer stare at a spinner?’ A timeout is also a case of ‘unknown’ rather than ‘no’, the same principle as in idempotency keys for API retries: when you did not get an answer, you do not know the outcome, and the code should say so instead of guessing.

The regression that wrote the rule

The ladder’s step 2, the dataset check, runs before any network call and exists because of a bug in an early release.

In that release the country-pattern dataset used for offline format checks lived only in a cache. When the cache expired while the upstream source was unreachable, there were no patterns left. With no patterns, no VAT number could match its country’s format, so every number came back invalid and checkout was blocked. That was precisely the failure the plugin promised could not happen.

The fix had two parts. The dataset is now bundled with the plugin, with a last-known-good copy kept in an option and the stale cache served while a refresh is scheduled. And the validator treats ‘I have no data’ as unverified, never as invalid. The comment in the code says why: calling it invalid would block every B2B checkout for as long as the source is unreachable.

The general rule I took from it: whatever your validation function returns for ‘I could not evaluate this’ must be a different value from what it returns for ‘this is wrong’. If your function returns a boolean, you have already lost that distinction. Mine returns a status, and the blocking decision is made from the status.

When fail-open is the wrong choice

Everything above rests on one property: letting the order through is cheap and recoverable. When that is not true, the same design is a bug.

  • Payments and fraud checks. If the API call decides whether a charge is legitimate, ‘no answer’ passing through is the harm itself.
  • Authorisation and entitlement. Letting an unchecked request through is the failure mode you are trying to prevent.
  • Anything irreversible. If you cannot correct the outcome afterwards, you cannot afford to guess.

For those, fail closed and spend the effort on making the outage short and visible. The test I use is to ask what the worst outcome of letting an unverified request through is, and who pays for it. For a B2B buyer who overpays a VAT charge they may recover, I am comfortable. For anything where the cost is a fraud loss or a data exposure, I am not.

The plugin has one more thing going for it: ‘unknown’ maps to the taxable default, not to the exemption. A failure can never grant a reverse charge, only withhold it. Where unknown would map to the permissive outcome, that is a different design.

Related service

API & Integrations

Deciding what a checkout does when a third-party API is slow, down or ambiguous is integration design. I make that call explicit before it becomes an incident.

More about this service →

Further reading:

  • EU VAT Rates for WooCommerce – WordPress Plugin – the plugin this article is about
  • vatnode.dev – EU VAT Validation API – the API behind the live validation step
  • EU VAT Number Validation in Next.js (VIES API Tutorial) – why raw VIES can say ‘invalid’ during an outage
  • Idempotency Keys: Building Retries That Don’t Double-Charge – treating a timeout as unknown
Iurii RoguliaAvailable

API & Integrations

Putting a third-party API in the path of a checkout and need to decide what happens when it fails? That is integration design I do before the first line of code.

More about this service

Relevant client work

View all projects
EU VAT Rates for WooCommerce – WordPress Plugin
EU VAT Rates for WooCommerce – WordPress Plugin
August 5, 2026
EU VAT Rates for WooCommerce – WordPress Plugin

WordPress plugin that keeps WooCommerce EU tax rates current and applies the B2B reverse charge from a VIES-verified VAT number at checkout.

kri.rocks – Booking Website and Online Scheduling SaaS for Beauty Businesses
kri.rocks – Booking Website and Online Scheduling SaaS for Beauty Businesses
September 28, 2026

Vertical SaaS that gives a salon, barbershop or solo beauty specialist their own booking website in minutes.

n8n Verified Community Node – EU VAT Validation in Workflows
n8n Verified Community Node – EU VAT Validation in Workflows
August 6, 2026
n8n Verified Community Node – EU VAT Validation in Workflows

Verified n8n node for VIES VAT validation and EU rate lookups in a workflow, so onboarding or invoicing acts on a real VAT number, not a well-formed one.

What clients say

“

Iurii built our entire B2B platform from scratch in a remarkably short time – a fully multilingual webshop covering 32 European markets in 28 languages, with automated VAT calculation, VIES

Mark Tamm 🇪🇪

Account Manager, pi-pi.ee

Services

StripeNotion

Topics

E-commerceB2Bi18nPaymentsPDFTax/VAT
“

Our Meta ad account had grown into a mess of duplicated audiences and ad sets nobody remembered creating.

Erik Lindgren 🇸🇪

E-commerce Manager

Topics

Meta AdsConversion TrackingE-commerce
“

We’d already paid for a generic SEO audit that listed hundreds of items with no priority order.

Camille Dubois 🇫🇷

Directrice Marketing

Topics

SEOi18nE-commerce

Related articles

Checkout Totals: Why the Server Recalculates Every Time, Not Once
October 21, 2026· 11 min
Checkout Totals: Why the Server Recalculates Every Time, Not Once

A cached VAT table and a courtesy FX conversion don’t make a checkout trustworthy – recalculating the total server-side does.

Stack

TypeScriptNext.js
Building an MCP Server for VAT Validation: Why It’s Two Tools, Not One
September 18, 2026· 9 min
Building an MCP Server for VAT Validation: Why It’s Two Tools, Not One

Building the vatnode MCP server meant deciding whether format-checking and live VIES validation should be one tool or two.

Stack

TypeScriptNode.js
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 with GitHub Actions – architecture, versioning, and per-ecosystem gotchas.

Stack

TypeScriptPythonPHPGoRuby
EU VAT Number Validation in Next.js (VIES API Tutorial)
February 5, 2026· 12 min
EU VAT Number Validation in Next.js (VIES API Tutorial)

EU VAT validation in Next.js: VIES caching with Redis, three-state results, reverse charge logic, and audit logging – what actually works in production.

Stack

Next.jsTypeScript

Databases

Redis
AI Translation Pipeline: Validators Around a gpt-4o Call
November 11, 2026· 13 min
AI Translation Pipeline: Validators Around a gpt-4o Call

An AI translation pipeline around model calls: a missing-key diff, ICU and plural validation before every write, and a glossary checked at commit time.

Stack

PythonTypeScriptNext.js