Stripe's VAT Verification Happens After Checkout, Not Before

2 September 2026

Stripe's VAT Verification Happens After Checkout, Not Before

Stripe Collects the VAT Number. VIES Validation Happens Later.

Stripe does check EU VAT numbers against VIES. That surprises people, because the checkout experience doesn't feel like it — the field accepts almost anything shaped like a VAT number, the session completes immediately, and the reverse charge decision is already made before Stripe has asked VIES anything. That's because Stripe's own docs describe two separate steps with two separate timings: a synchronous format check at checkout, and an asynchronous VIES check afterward — and only the first one gates anything.

If you sell to EU businesses and apply reverse charge based on the tax ID Stripe collects, the gap between those two steps is the thing worth understanding.

What Stripe actually verifies at checkout

Stripe's documentation on collecting tax IDs in Checkout (as of 20 August 2026) is direct about this:

"During the Checkout Session, Stripe verifies that the provided tax IDs are formatted correctly, but not that they're valid. You're responsible for ensuring the validity of customer information collected during checkout."

A format check confirms the string matches the pattern for an eu_vat tax ID — right prefix, right length, right character set. It does not confirm the number is registered, active, or that it belongs to the business typing it in. A syntactically perfect but entirely made-up VAT number passes this check.

And the tax treatment is decided right here, on the format check alone:

"If you use Stripe Tax and your customer provides a tax ID, Stripe Tax applies the reverse charge or zero rate according to applicable laws, as long as the tax ID conforms to the necessary number format, regardless of its validity."

That's the core mechanic. Checkout completes, the invoice is issued with reverse charge applied, and the only thing that decision was based on is shape.

The VIES check happens after checkout, not before

Stripe does run a real check — it's just not on the critical path. Stripe's documentation on account and customer tax IDs (same effective date) covers it:

"Stripe checks the format of the tax ID against the expected format, and asynchronously validates the tax ID against the external tax authority system for the tax ID types below," including "European Value Added Tax (EU VAT) numbers" against VIES.

"Asynchronously" means after the Checkout Session has already completed and the customer has already been charged. Stripe notes the VIES call "usually takes only a few seconds, but might take longer, depending on the availability" of VIES — and it inherits VIES's own lack of an SLA, so "a few seconds" is not a guarantee.

When the result lands, Stripe surfaces it in two places, not before:

  • The customer.tax_id.updated webhook — "Because this validation process happens asynchronously, the customer.tax_id.updated webhook notifies you of validation updates." This is the event to listen for, not customer.tax_id.created, which fires on collection, before any VIES call has happened.
  • A Dashboard tooltip — hovering over the customer's EU VAT number shows the VIES result (registered name and address, where VIES returns them) as an object attribute on the tax ID.

Both are read-only views of a result that already shipped. Nothing about that result changes the invoice, the reverse charge decision, or the checkout that already happened.

Once, not ongoing

The check also doesn't repeat. Stripe is explicit on the invoicing tax-ID page:

"After a tax ID is confirmed as valid or invalid, it won't be validated again automatically."

And on the Billing customer tax-ID page: "we don't continue to validate them over time. If automatic validation isn't available, you must manually verify these IDs."

A VAT registration that was valid on day one and gets deregistered eight months later — company closed, VAT number cancelled, business deregistered from the scheme — stays marked however Stripe last saw it. Stripe never asks VIES again on its own.

What this leaves you with

Put the three points together and the shape of the gap is clear:

  1. Reverse charge is applied on format alone, before Stripe has asked VIES anything.
  2. The real check completes later, with no mechanism to un-apply reverse charge or re-open the invoice if the result is a surprise.
  3. The check runs once, so a number that degrades after collection is never caught.

None of this is a defect in Stripe — collecting a tax ID inline and deciding tax treatment synchronously is the only way checkout stays fast, and Stripe says as much: verifying validity is your responsibility, not something checkout blocks on. Stripe is complementary infrastructure for tax IDs and invoicing, not a VAT validation service, and its docs don't claim otherwise.

There's also a narrower, quieter gap: Stripe's docs do not describe surfacing a VIES consultation number — the requestIdentifier VIES issues on a requester-qualified lookup, which is contemporaneous evidence that a specific check happened on a specific date. What you get instead is a status (verified, unverified, or pending) and a name/address snapshot in the Dashboard — useful, but not a reference you can hand an auditor as proof a particular lookup occurred.

One scope note if you sell physical goods rather than SaaS: the VAT ID plays a different structural role there. It's a substantive condition for the intra-Community exemption (goods leaving the member state), not "reverse charge" in the strict cross-border-services sense — but Stripe's format-only gate at checkout behaves the same either way, so the gap described here applies to both.

Closing the gap without touching your Stripe flow

The fix isn't to replace Stripe's tax ID collection — keep that, it's the right UX for checkout. The fix is to run a synchronous, requester-qualified VIES check of your own right when Stripe hands you the tax ID, ahead of Stripe's own async round-trip, and store the result as evidence you actually control instead of relying only on Stripe's tooltip.

The hook is checkout.session.completed — the same event where the collected tax ID is already sitting on customer_details.tax_ids, before Stripe has asked VIES anything. Call the VAT validation endpoint synchronously right there, and persist the answer alongside the invoice.

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function handleStripeWebhook(req: Request) {
  const sig = req.headers.get('stripe-signature')!
  const body = await req.text()

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch {
    return new Response('Invalid signature', { status: 400 })
  }

  if (event.type !== 'checkout.session.completed') {
    return new Response('ok', { status: 200 })
  }

  const session = event.data.object as Stripe.Checkout.Session
  const taxId = session.customer_details?.tax_ids?.find((t) => t.type === 'eu_vat')
  if (!taxId?.value || !session.customer) {
    return new Response('ok', { status: 200 })
  }

  const res = await fetch(`https://api.vatnode.dev/v1/vat/${taxId.value}`, {
    headers: { Authorization: `Bearer ${process.env.VATNODE_API_KEY}` },
  })

  if (!res.ok) {
    // VIES_UNAVAILABLE (503), UPSTREAM_TIMEOUT (504), RATE_LIMITED (429), etc.
    // Upstream trouble, not an invalid number — queue a retry, don't touch the invoice.
    await queueVatRecheck(session.customer as string, taxId.value)
    return new Response('ok', { status: 200 })
  }

  const check = await res.json()
  // { valid, vatId, countryCode, countryName, companyName, companyAddress,
  //   source, consultationNumber, checkId, verifiedAt }

  await db.insert('vat_checks', {
    customer_id: session.customer,
    vat_id: check.vatId,
    valid: check.valid,
    source: check.source,
    consultation_number: check.consultationNumber, // null on cache hits and national fallback
    check_id: check.checkId,
    verified_at: check.verifiedAt,
  })

  if (!check.valid) {
    await flagInvoiceForReview(session.customer as string)
  }

  return new Response('ok', { status: 200 })
}

A few things worth calling out in that handler:

  • It's gated on checkout.session.completed, so it runs once per real checkout — not on every keystroke — but it runs immediately, without waiting on Stripe's own async VIES round-trip.
  • A non-200 from the API means VIES (or the network) had a problem, not that the number is wrong. Treat those the same way you'd treat any other VIES downtime — retry, don't reject and don't touch tax treatment on a guess.
  • consultationNumber is only non-null when the lookup went through the requester-qualified path and VIES actually answered; it's null on a cache hit and on every national-fallback result, since a national answer isn't a VIES answer. Some countries have that fallback for when VIES is briefly unavailable, and the coverage page lists which. The requester-qualified path itself is zero-config on vatnode's side: get a free API key, set your own EU VAT ID once as requester in dashboard Settings, and every call after that — including this handler — returns the consultation number, without wiring up checkVatApprox yourself.
  • flagInvoiceForReview is deliberately not "reverse the reverse charge automatically." Whether to re-issue, credit, or just flag for manual follow-up is a billing-process decision, and by the time this fires the invoice may already be final.

This one handler closes the first two gaps — gating on format alone, and no evidence of your own — but not the third: a number that was fine at checkout and degrades six months into the subscription. Neither Stripe nor a one-off check at checkout catches that, because neither re-checks after the fact. That's a separate, deliberate re-check on a schedule — ongoing VAT monitoring subscribes a VAT ID once and fires its own webhook the moment a live registration changes state.

This runs alongside Stripe, not instead of it — Stripe still owns collection, invoicing, and payment. What changes is that your reverse charge decision now has a synchronous, requester-qualified answer with a checkId and (when available) a consultation number behind it, instead of a format check plus an eventual tooltip. The full end-to-end integration — where in the checkout flow to hook in, how to wire the format precheck client-side, how to handle the tax_exempt field on the customer — is covered in the Stripe VAT validation guide; this post is about the timing gap specifically.

If you're weighing whether to build this yourself against Stripe's async result at all, the honest tradeoffs — what a thin in-house check gets you versus what it doesn't — are in build vs buy for VAT validation: the free, built-in path has real gaps, and knowing which ones matter for your case is most of the decision.

This is general information about EU VAT and VIES, not tax advice. A valid VIES result — from vatnode or from Stripe's own async check — confirms a number was registered on the date it was checked; it doesn't certify tax compliance or, by itself, settle whether a given transaction actually qualifies for reverse charge or exemption. Never block a checkout or a payment on VIES being slow or unavailable — unavailable is not the same as invalid. Confirm the treatment of your own transactions with a qualified tax adviser.

FAQ

Does Stripe validate EU VAT numbers?

Yes — Stripe automatically checks EU VAT numbers against VIES, but the check runs asynchronously after the number is collected, not before checkout completes.

Does Stripe block checkout if the VAT number turns out to be invalid?

No. Checkout only verifies the format at collection time; Stripe Tax applies the reverse charge based on that format regardless of what the later VIES check finds.

Where can I see the result of Stripe's VAT verification?

In the Stripe Dashboard, as a tooltip on the customer's tax ID, or via the customer.tax_id.updated webhook — you get a status (verified, unverified, or pending) but no VIES consultation number and no reproducible check record you control.

Does Stripe re-check a VAT number after it's saved?

No — Stripe validates once, at collection, and doesn't continue validating it over time, so a registration that later becomes invalid won't be reflected in what Stripe stored.

Run a synchronous check alongside Stripe

The EU VAT validation API runs the requester-qualified VIES call synchronously, falls back to a national source on downtime, and returns the evidence fields in one response — including the consultation number. Get a free API key: free plan, 100 requests/month, about one check per checkout that carries a VAT ID.