<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VatPulse</title>
    <description>The latest articles on DEV Community by VatPulse (@vatpulse).</description>
    <link>https://dev.to/vatpulse</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191673%2F9cc73741-cf8e-4795-a510-e7dba2fd0183.png</url>
      <title>DEV Community: VatPulse</title>
      <link>https://dev.to/vatpulse</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vatpulse"/>
    <language>en</language>
    <item>
      <title>VIES Is Down Again: Keeping EU VAT Checks Working During VIES Downtime</title>
      <dc:creator>VatPulse</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:43:58 +0000</pubDate>
      <link>https://dev.to/vatpulse/vies-is-down-again-keeping-eu-vat-checks-working-during-vies-downtime-4apk</link>
      <guid>https://dev.to/vatpulse/vies-is-down-again-keeping-eu-vat-checks-working-during-vies-downtime-4apk</guid>
      <description>&lt;p&gt;If you sell B2B in the EU, you probably call &lt;strong&gt;VIES&lt;/strong&gt; (the European Commission's VAT Information Exchange System) to check a customer's VAT number before applying the reverse charge. And if you have done that for more than a few weeks, you have seen this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actionSucceed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorWrappers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MS_UNAVAILABLE"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your checkout freezes, your invoicing job retries forever, or worse, your code treats "VIES didn't answer" as "this VAT number is invalid" and charges a legitimate business customer VAT they shouldn't pay.&lt;/p&gt;

&lt;p&gt;This post explains why VIES downtime happens, the different failure modes, and a pattern that keeps VAT checks working anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  VIES is a router, not a database
&lt;/h2&gt;

&lt;p&gt;The key thing to understand: VIES does not hold VAT data itself. When you ask it about &lt;code&gt;DE123456789&lt;/code&gt;, it forwards the request to Germany's national system and relays the answer. So there are really &lt;strong&gt;28 services&lt;/strong&gt; that can fail (27 member states + Northern Ireland via &lt;code&gt;XI&lt;/code&gt;), plus the central hub.&lt;/p&gt;

&lt;p&gt;That gives you several distinct failure modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VIES error&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What you should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MS_UNAVAILABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;That member state's system is offline (often maintenance, frequently outside business hours)&lt;/td&gt;
&lt;td&gt;Retry later; other countries still work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The member state didn't answer in time&lt;/td&gt;
&lt;td&gt;Retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SERVICE_UNAVAILABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The central VIES service is down&lt;/td&gt;
&lt;td&gt;Retry later; nothing works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MS_MAX_CONCURRENT_REQ&lt;/code&gt; / &lt;code&gt;GLOBAL_MAX_CONCURRENT_REQ&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You (or everyone) are being throttled&lt;/td&gt;
&lt;td&gt;Back off, then retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INVALID_INPUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The number is malformed&lt;/td&gt;
&lt;td&gt;Don't retry, the input is wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can see live per-country availability yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://ec.europa.eu/taxation_customs/vies/rest-api/check-status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Saturday evening I wrote this, it listed Germany as &lt;code&gt;"Unavailable"&lt;/code&gt; while every other member state was up. That's normal VIES behaviour, not a one-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three mistakes almost everyone makes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Treating "unavailable" as "invalid."&lt;/strong&gt; A VIES outage tells you nothing about the VAT number. Collapsing both into &lt;code&gt;valid: false&lt;/code&gt; blocks real customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Calling VIES on every request.&lt;/strong&gt; The same customer's VAT number gets checked at signup, at every checkout, and on every invoice. Every one of those calls is another chance to hit an outage or a throttle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No memory of past answers.&lt;/strong&gt; If you confirmed &lt;code&gt;IE6388047V&lt;/code&gt; was valid yesterday, that's very strong evidence it's still valid today. Throwing that away the moment VIES hiccups is wasteful.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pattern that survives VIES downtime
&lt;/h2&gt;

&lt;p&gt;The fix is a small, boring layer between your app and VIES:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Syntax check first.&lt;/strong&gt; Every country has a known format (&lt;code&gt;ATU&lt;/code&gt; + 8 digits, &lt;code&gt;NL&lt;/code&gt; + 9 digits + &lt;code&gt;B&lt;/code&gt; + 2 digits, ...). Reject obvious typos locally and never bother VIES.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh cache (24h).&lt;/strong&gt; Store each successful answer for a day. Repeat checks are instant and don't touch VIES at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale cache (30 days).&lt;/strong&gt; Keep a longer-lived "last known good" copy. If VIES or the member state is down, serve that copy and &lt;em&gt;flag it clearly&lt;/em&gt; as stale with the original check timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit errors otherwise.&lt;/strong&gt; If you have never seen the number and VIES is down, return a distinct &lt;code&gt;service_unavailable&lt;/code&gt; or &lt;code&gt;member_state_unavailable&lt;/code&gt; error, never &lt;code&gt;valid: false&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your application logic then becomes simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nf"&gt;applyReverseCharge&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;chargeVat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;queueForRecheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// unknown: don't guess&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Doing it with one API call
&lt;/h2&gt;

&lt;p&gt;You can build this layer yourself; it is not much code. I also packaged it as a hosted API, &lt;strong&gt;&lt;a href="https://rapidapi.com/vatpulse/api/vatpulse1" rel="noopener noreferrer"&gt;VatPulse&lt;/a&gt;&lt;/strong&gt;: one endpoint, VIES under the hood, with the caching and fallback described above. The examples below use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://vatpulse1.p.rapidapi.com/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RAPIDAPI_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: vatpulse1.p.rapidapi.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"vat": "DE811907980"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Germany is down but there's a recent cached answer, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vat_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE811907980"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-15T09:12:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"member_state_unavailable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The member state's VAT database is temporarily unavailable via VIES. ..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"billable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;billable: false&lt;/code&gt;: cache hits, stale fallbacks and errors don't count against the quota. Only fresh, successful validations are billed.&lt;/p&gt;

&lt;p&gt;If nothing is cached, you get an honest HTTP 503 with &lt;code&gt;member_state_unavailable&lt;/code&gt; and a &lt;code&gt;Retry-After&lt;/code&gt; header, so your job queue knows exactly what to do.&lt;/p&gt;

&lt;p&gt;You can also check live per-country status before a batch run (public endpoint, no key needed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.vatpulse.workers.dev/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What about compliance?
&lt;/h2&gt;

&lt;p&gt;Serving a stale result is a business decision. For most SaaS and e-commerce flows, "this number was confirmed valid by VIES 2 days ago" is a perfectly reasonable basis to proceed and re-check later. Because every response includes &lt;code&gt;checked_at&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt; and a &lt;code&gt;stale&lt;/code&gt; flag, you can store an audit trail and decide your own policy, for example "accept stale results up to 7 days old, otherwise hold the invoice."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free tier (100 validations/month) on RapidAPI: &lt;a href="https://rapidapi.com/vatpulse/api/vatpulse1" rel="noopener noreferrer"&gt;https://rapidapi.com/vatpulse/api/vatpulse1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://api.vatpulse.workers.dev/docs" rel="noopener noreferrer"&gt;https://api.vatpulse.workers.dev/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built VatPulse, the API used in the examples above. The patterns work just as well if you build them yourself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tax</category>
      <category>api</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
