Validate VAT Numbers on Subscription Renewal, Not Purchase

16 September 2026

Validate VAT Numbers on Subscription Renewal, Not Purchase

Validate VAT Numbers on Subscription Renewal, Not Purchase

A VAT ID checked once at purchase tells you the number was valid on that day. It says nothing about the day the next invoice is generated. For a subscription, that’s a problem: every renewal is a new supply, and the EU VAT Directive’s reverse-charge basis for treating a customer as a business rests on evidence obtained for that supply – not evidence obtained months earlier for a different one. This post is about the trigger that closes that gap: hooking your own billing provider’s renewal event and re-checking the VAT ID right before the next invoice is built.

The scope is a recurring B2B SaaS subscription – an electronically supplied service, EU-27. Goods get one aside near the end; the rest of this post is services.

Where this sits among the other triggers

There are three obvious moments to check a customer’s VAT number, and each answers a different question:

  • Signup – a stored account attribute, checked once, off the critical path of account creation. It tells you the number was valid when the account was created.
  • Checkout – a single cart total, checked live for one order. It tells you the number was valid for that specific purchase.
  • The periodic sweep – you bulk re-validate your customer VAT IDs across the whole base on a schedule, the canonical way to clean up drift after the fact.

Renewal is the trigger between checkout and the bulk sweep: it re-checks the same number, but on the exact cadence that matches when a new invoice – and a new supply – is about to happen, rather than waiting for the next scheduled backfill to notice.

Why a number valid at purchase isn’t valid forever

A VAT registration is not a permanent fact. In the time between the first invoice and the twelfth renewal, a customer can voluntarily deregister, get administratively struck off for non-compliance, go through insolvency, or restructure into an entity that issues a new number entirely. In some member states, a business can also have its intra-EU (VIES) trade status suspended while its domestic VAT number stays active for local invoicing – Spain’s ROI is a documented example of this split. None of that generates a notification to you. The value sitting in your customers table is a snapshot of a moment that has already passed by the time the next renewal fires.

There’s no EU rule that says how often you have to re-check. Cadence is a risk decision, made with a tax adviser, not a legal deadline. But the billing cycle is a natural place to hang that decision – it’s the moment a new invoice, and a new supply, is about to happen anyway, so the check costs you nothing you weren’t already going to spend time on.

This is general information about EU VAT and VIES, not tax advice. Whether a specific renewal invoice should carry VAT or qualify for reverse charge depends on facts we can’t assess here — confirm the treatment of your own subscriptions with a qualified tax adviser.

What a renewal-time check is actually evidence for

For a recurring B2B SaaS subscription, VAT is normally due where the customer is established under Article 44 of the VAT Directive, with the customer self-accounting for the VAT under the reverse charge, Article 196. The valid VAT number is part of the evidence you rely on (Implementing Regulation 282/2011, Art 18) to treat that customer as a business under that rule – for one invoice at a time.

A registration that stops validating at renewal is a compliance-risk signal for the next invoice. It is not an automatic reclassification, and it is not proof the invoicing basis has to change on its own – the number could be a false negative from a temporary outage, a customer who restructured and just hasn’t sent you the new ID yet, or a genuine deregistration. Route it to a human, not straight to a billing change.

One aside for goods, since it’s a different regime entirely: ‘reverse charge’ (Art 196) is the services term. For goods, the parallel mechanism is Art 138 zero-rating of the intra-Community supply, with the valid VAT ID a substantive condition since the 2020 Quick Fixes – don’t call that ‘reverse charge.’ This post stays on services.

VIES valid means the number’s status in the national database at the moment of the check – it’s a live query against national databases, not a permanent certification, and the European Commission does not guarantee its accuracy, because the underlying data and control sit with each member state. Format-valid is not the same thing as VIES-valid, and a good answer today is not a guarantee for the renewal after this one.

Hooking your billing provider’s renewal event

The pattern doesn’t need vatnode’s own monitoring product. It’s application code that lives in your billing lifecycle: your PSP fires a pre-renewal event a few days before the next invoice is finalized – Stripe calls it invoice.upcoming; most other billing providers expose an equivalent. On that event, you make one on-demand GET /v1/vat/:vatId call for the customer’s stored VAT ID, before the renewal invoice is generated, and branch the tax treatment on the result. Single calls like this run fine on the free plan.

// billing/vat-renewal-check.ts — server-side only, called from your billing
// provider's pre-renewal webhook. Framed on Stripe's `invoice.upcoming`;
// use your own PSP's equivalent event if it isn't Stripe.

type RenewalCheckResult =
  | { status: 'valid'; checkId: string; consultationNumber: string | null }
  | { status: 'invalid'; checkId: string }
  | { status: 'pending'; reason: string } // could not check yet — retry later
  | { status: 'needs_attention'; reason: string } // your own data/config, not retryable

async function checkVatOnRenewal(vatId: string): Promise<RenewalCheckResult> {
  let res: Response
  try {
    res = await fetch(`https://api.vatnode.dev/v1/vat/${encodeURIComponent(vatId)}`, {
      headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` },
      // No user is waiting on a renewal check, so give VIES its full ~10s
      // server-side window plus headroom for the requester consultation-number
      // retry — not the tight timeout a checkout call would use.
      signal: AbortSignal.timeout(15000),
    })
  } catch {
    // Network failure or timeout — same "no answer" class as an outage.
    return { status: 'pending', reason: 'NETWORK' }
  }

  if (!res.ok) {
    const { error } = await res.json()
    switch (error.code) {
      case 'RATE_LIMITED':
      case 'VIES_UNAVAILABLE':
      case 'UPSTREAM_TIMEOUT':
      case 'VIES_ERROR':
      case 'INTERNAL_ERROR':
        // VIES (or the request) didn't answer — retryable, not a verdict.
        return { status: 'pending', reason: error.code }
      case 'INVALID_FORMAT':
        // The stored VAT ID itself is malformed — retrying won't fix that.
        // Flag the customer record for someone to correct, don't requeue.
        return { status: 'needs_attention', reason: error.code }
      case 'INVALID_REQUESTER':
        // Your own requester-VAT config, not the customer's number — also
        // not something a retry resolves. Alert ops.
        return { status: 'needs_attention', reason: error.code }
      case 'UNAUTHORIZED':
      case 'INVALID_API_KEY':
        // Missing or revoked API key — your own config again, and a retry
        // won't fix it. Alert ops rather than requeue forever.
        return { status: 'needs_attention', reason: error.code }
      default:
        return { status: 'pending', reason: error.code }
    }
  }

  const data = await res.json()
  // { valid, vatId, countryCode, countryName, companyName, companyAddress,
  //   checkId, verifiedAt, source, consultationNumber } — see /docs for the
  // full response shape.

  return data.valid
    ? { status: 'valid', checkId: data.checkId, consultationNumber: data.consultationNumber }
    : { status: 'invalid', checkId: data.checkId }
}

Only valid and invalid are real outcomes. A transient outage – 429 RATE_LIMITED, 503 VIES_UNAVAILABLE, 504 UPSTREAM_TIMEOUT – is pending and worth retrying, the same discipline used for bulk re-validation. A permanent problem on your side – INVALID_FORMAT on a stored ID, INVALID_REQUESTER on your own config – isn’t going to fix itself on a retry, so it routes to needs_attention instead of sitting in a retry queue forever. Neither one is a negative result, and neither gets to flip the customer’s tax treatment.

The webhook handler wires that result into the renewal:

// Stripe's `invoice.upcoming` fires a few days before the renewal invoice is
// finalized — the last convenient moment to re-check the VAT ID before the
// next period's invoice is generated.
async function handleInvoiceUpcoming(event: { customerId: string; vatId: string | null }) {
  if (!event.vatId) return // no VAT ID on file — nothing to re-check

  const result = await checkVatOnRenewal(event.vatId)

  switch (result.status) {
    case 'valid':
      await recordVatCheck(event.customerId, {
        valid: true,
        checkId: result.checkId,
        consultationNumber: result.consultationNumber,
      })
      // Existing reverse-charge treatment stands for the next invoice.
      break

    case 'invalid':
      await recordVatCheck(event.customerId, { valid: false, checkId: result.checkId })
      // Flag for review — do NOT flip the billing treatment automatically.
      // The number could be a genuine deregistration, a restructuring that
      // needs a new ID from the customer, or a false negative worth retrying.
      await flagForVatReview(event.customerId)
      break

    case 'pending':
      // Could not check yet. Keep the previous treatment on this invoice
      // and requeue — never store a pending result as invalid.
      await requeueVatRecheck(event.customerId, result.reason)
      break

    case 'needs_attention':
      // Our own data or config, not the customer's number. Keep the
      // previous treatment, alert ops — requeuing this won't help.
      await alertOpsVatConfig(event.customerId, result.reason)
      break
  }
}

The invalid branch routes to review, not to an automatic ‘now charge VAT’ change. Whether that renewal should actually carry VAT is fact-specific – it depends on why the check failed, whether the customer has since re-registered under a different number, and what your own risk tolerance is – so it belongs with a person (and, for anything material, a tax adviser), not a switch statement.

Not vatnode’s own monitoring – and that’s the point

vatnode also ships an always-on monitoring product that looks similar from a distance. The two are mechanically different things:

  • The renewal hook in this post is code you write, triggered by your billing provider’s event, making a single on-demand GET /v1/vat/:vatId call once per billing cycle. It runs on the free plan – there’s no subscription-monitoring feature involved, just the plain validation endpoint called at a moment you chose.
  • Continuous VAT monitoring with webhooks is vatnode registering a VAT ID via POST /v1/subscriptions and re-checking it automatically on its own schedule, firing a webhook to you when the status changes. That’s a Starter-plan-and-up feature, independent of your billing cycle – see the monitoring API reference for the setup.

If you’d rather not build and maintain the renewal hook yourself, registering the same customer VAT IDs for continuous monitoring is the always-on alternative – you get notified the moment a number changes instead of waiting for your next renewal to notice. Which one fits depends on whether you want the check tied to your billing cycle specifically, or decoupled from it entirely.

Consultation numbers are a VIES-only artifact

When your requester VAT is configured in dashboard Account details, a fresh VIES answer carries a consultationNumber – proof a specific check ran through the official EU system at a specific time, useful evidence to keep alongside the renewal record.

That evidence only exists for VIES-sourced results. A small set of member states have a national registry that can decide valid/invalid during a VIES outage – see coverage for which – and those national-fallback checks always carry consultationNumber: null. So ‘every renewal check gives you audit evidence’ only holds when source is VIES; a national-fallback result is still a real, timestamped answer, just of a different evidentiary weight. More on what the consultation number does and doesn’t certify: the VIES consultation number explained.

One scope note: XI (Northern Ireland) is a goods-only VIES prefix. A SaaS subscription billed to a Northern Ireland business follows UK VAT rules, not Article 44/196 – it isn’t the case this post is about.

FAQ

Why re-validate a VAT number at renewal if I already checked it at purchase?

A subscription is a series of separate supplies, not one event – the VAT ID you confirmed at purchase is evidence for that invoice, not for the one twelve months later. Registrations lapse in between, and nothing notifies you when it happens.

Does an invalid VAT number at renewal mean I have to start charging VAT?

Not automatically. A failed re-check at renewal is a compliance-risk signal for the next invoice, not proof the invoicing basis has to change – route it to a human review (or the customer, to confirm their status) before you touch the billing treatment.

How is a renewal-trigger check different from vatnode’s VAT monitoring?

The renewal hook is your own code – a single on-demand GET /v1/vat/:vatId call fired from your billing provider’s renewal event, and it runs fine on the free plan. vatnode’s own monitoring (POST /v1/subscriptions plus webhooks) re-checks registered numbers automatically on its own schedule and is a Starter-plan-and-up feature – you don’t need it to build the pattern in this post.

Is there an EU rule on how often I must re-check a customer’s VAT number?

No. There’s no EU-mandated re-check interval – cadence is a risk decision you make with a tax adviser. The billing cycle is just a convenient, low-cost trigger to hang that decision on, not a legal deadline.

What should my code do if VIES is unreachable during the renewal check?

Treat it as pending, not invalid. Keep the customer’s existing VAT treatment on that invoice, requeue the check, and only ever act on a check that actually completed with valid true or valid false.

Add the renewal check to your billing lifecycle

The EU VAT validation API is a single GET /v1/vat/:vatId call with a Bearer key – call it from your own renewal webhook, no subscription setup required. Free plan, 100 requests/month. If you’d rather not trigger it yourself, continuous VAT monitoring with webhooks does it on its own schedule from Starter up. Stripe’s own VAT verification runs after checkout, which is a different timing problem than the one this post solves.

Get a free API key · API reference