Skip to Content
EU B2B Checkout

Validating an EU B2B checkout

A signup form can afford to be unsure. A checkout cannot: it has to decide the tax treatment and the payment method in the same second, and both decisions are auditable afterwards. Charge VAT you should have reverse-charged and your customer notices on the invoice. Reverse-charge for a number that was never registered and you owe the VAT yourself.

Two fields carry those decisions, and each fails in a way a boolean cannot express:

FieldThe decisionHow it fails
VAT numberReverse charge, or charge VAT?The registry that answers it is down several times a month
IBANCan I collect this by direct debit?The number is perfect and the country is outside SEPA

This page is about those two failures, because they are the ones that reach production. Email and phone were settled at signup — that form is a different problem.


The VAT decision needs registered, not well formed

A structurally valid VAT number tells you nothing about the tax treatment. Reverse charge under Article 138 needs the customer to be registered for intra-community operations in a member state other than yours — and the only thing that knows is VIES.

So branch on vatDetails.registered, never on valid:

{ "valid": true, "message": "Valid VAT number", "vatDetails": { "format_valid": true, "registered": true, "country_code": "IE", "source": "LIVE", "consultation_number": "WAPIAAAAX8k1abcd" } }

Three fields matter at a checkout:

  • registered — true, false, or null. null means unknown, never “no”. This is the whole point of the next section.
  • country_code — compare it with your own establishment. A French seller charging a French customer charges French VAT, however valid that number is.
  • consultation_number — the receipt VIES issues, and the evidence a tax authority accepts that you checked on that date. It is only issued to an identified requester, so it is present only when your own VAT number is configured on the account. Store it with the order, not in a log that rotates. Every plan can read it; how long we keep our copy depends on the plan.

Two member states out of 28 cannot answer right now

This is the part a checkout has to be designed around rather than hoped away.

Measured from our own VIES probe on 20 September 2026, over a rolling 30-day window — 28 member states, 4,418 samples each, 123,704 in total, one reading per state every ten minutes:

Median availability across member states99.73 %
Mean availability97.53 %
Member states below 95 %5
Member states unavailable at the moment of reading2

And the distribution is what hurts, because it is not uniform:

Member stateAvailability, 30 days
Latvia81.96 %
Germany82.98 %
Lithuania89.45 %
Belgium91.72 %
Cyprus94.05 %

Germany is the largest B2B market in the Union and the second-least reliable registry in it. A checkout that refuses the order when VIES cannot answer refuses roughly one German B2B order in six.

These figures come from the same probe as How often is VIES actually down?, read live at /api/v1/status/vies. It records what our own calls to VIES returned, not an independent audit of each national registry — a network path between us and Brussels counts as unavailable here, exactly as it would for you.

What to do with registered: null

There is no technically correct answer, because this is a tax-risk decision, not an engineering one. There are two defensible policies, and one that is neither:

Charge VAT, refund on confirmation. Treat unknown as not-registered, charge the VAT, re-check asynchronously, and credit the customer when the registry confirms. You are never short. The cost is an invoice correction and a conversation. Which rate to charge is yours to decide — the member states’ rates are at /api/v1/vat/rates, without a key.

Accept the reverse charge, re-check after the fact. Better checkout conversion, and you carry the exposure: if the number turns out to be unregistered, the VAT was yours to collect. Defensible when the amount is small relative to the friction, and only with the re-check actually built.

Blocking the checkout is the one that is not defensible. It converts someone else’s outage into your lost order, and it does so most often in your biggest market.

Whichever you pick, record source with the order — LIVE, CACHE, STALE, UNVERIFIED or NOT_APPLICABLE. Six months later it is the difference between “we checked” and “we think we checked”.

import { VerifNow } from '@verifnow/sdk'; const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! }); type TaxTreatment = | { kind: 'reverse-charge'; receipt?: string } | { kind: 'charge-vat'; reason: string }; /** Our own establishment. Reverse charge needs the customer to be somewhere else. */ const SELLER_COUNTRY = 'FR'; export async function decideTax(vatNumber: string): Promise<TaxTreatment> { const result = await client.validateVat(vatNumber); const vat = result.vatDetails; if (!vat?.formatValid) { return { kind: 'charge-vat', reason: result.message ?? 'Not a valid VAT number' }; } if (vat.countryCode === SELLER_COUNTRY) { return { kind: 'charge-vat', reason: 'Domestic sale' }; } if (vat.registered === true) { return { kind: 'reverse-charge', receipt: vat.consultationNumber }; } if (vat.registered === false) { return { kind: 'charge-vat', reason: 'Not registered for intra-community operations' }; } // null: VIES could not answer. Charge the VAT, keep the source, re-check tonight. await queueVatRecheck({ vatNumber, source: vat.source }); return { kind: 'charge-vat', reason: `Registration unconfirmed (${vat.source})` }; }

Needs @verifnow/sdk 1.7.0 or later. For Java, io.verifnow:verifnow-spring 2.7.0 or later — getVatDetails().registered() is a Boolean, and it is nullable for this exact reason.


A perfect IBAN you cannot collect from

The SWIFT registry covers 89 countries. The SEPA schemes cover 42 of them. Everything in between is an IBAN that passes every check a validator can run and that no direct debit mandate will ever collect from:

{ "valid": true, "message": "Valid IBAN", "ibanDetails": { "country_code": "EG", "sepa": false, "structure_valid": true, "checksum_valid": true } }

valid stays true, and it should: the number is correct, and a bank transfer from Egypt works. What changes is the payment method you can offer. So sepa is the field a checkout branches on, not valid.

The membership list is where hand-written versions go wrong. Ours comes from the EPC’s own list of SEPA scheme countries  — EPC409-09 v8.0, 24 December 2025 — folded onto the ISO codes accounts actually carry:

In SEPA, and easy to missNot in SEPA, and easy to assume
GB — the UK stayed in after BrexitFK — an IBAN country, not a SEPA one
CH, LI — never in the EUTR, EG, IL, SA
GI, VA, MC, SM, ADBR, RU, UA
MD, AL, ME, MK (2025), RS (May 2026)

Guernsey, Jersey and the Isle of Man are in SEPA and issue GB IBANs; Réunion and Saint-Pierre-et-Miquelon FR ones; the Azores and Madeira PT ones. Forty-two codes cover fifty-six countries and territories.

sepa: true is permission, not reachability. It says the country is in scope, so a bank there may have joined the schemes. Whether this particular bank has, and answers SDD Core, is published per bank — nothing in an IBAN records it. Treat sepa: false as a hard no, sepa: true as worth attempting, and a failed first collection as the remaining answer.

In Spring, that decision is an annotation:

public record CheckoutForm( @VerifNowVat String vatNumber, @VerifNowIban(requireSepa = true) String iban) { }

requireSepa is off by default, because a payout or a transfer is happy to go anywhere. Turn it on for the field you will debit.


Two calls at submit, not five

There is no batch endpoint — one call validates one value. At a checkout that is less of a problem than it sounds, because the fields do not all need re-checking at submit:

  • While the user types, validate the field they just left. An IBAN corrected in the form costs nothing; one corrected by email three days later costs a support ticket.
  • At submit, re-check the two that decide something: the VAT number (tax treatment) and the IBAN (payment method). Two calls, in parallel.

That matters because concurrent requests are capped per account — 3 on Free, 5 on Starter, 10 on Growth, 25 on Pro. Two parallel calls fit on every plan; firing five at once on Free does not, and a 429 at submit is the worst possible moment to discover it.

const [tax, bank] = await Promise.all([ decideTax(form.vatNumber), client.validateIban(form.iban), ]);

If a call fails outright, the error handling guide has the shape: a 429 carries Retry-After, a 5xx is retried by the Node SDK automatically, and neither should take the order down with it.


What none of this tells you

Worth writing on the wall before the first invoice dispute:

  • That the company is the one your customer says it is. VIES returns a trading name and address for some member states and nothing for others; matching them is your judgement, not a verdict.
  • That the bank account belongs to that company. Confirming a holder needs SEPA name-checking or a penny-drop.
  • That the mandate will be honoured. A first collection is the only real test.
  • That a registered: true from yesterday still holds. Registrations are withdrawn. The receipt proves what you saw on the day, which is what a tax authority asks for — not that it is still true.

In short

  • Branch on vatDetails.registered, not on valid, and keep the consultation_number with the order.
  • registered: null means unknown. Pick a policy — charge and refund, or accept and re-check — and never block the checkout on someone else’s outage. In Germany that is one order in six.
  • Branch on ibanDetails.sepa for anything you will debit. A valid IBAN outside SEPA is a valid IBAN you cannot collect from.
  • Validate as the user types; at submit, two calls decide everything.

Where this leaves you

The reason this page can answer both halves is that both fields come from one API. A VAT specialist answers the first and has nothing to say about the second; an IBAN library answers the second offline and has never heard of VIES. A checkout needs one call for the tax decision, one for the payment decision, one key, one quota and one bill — and the same receipt trail behind both.

Sources: EPC List of SEPA Scheme Countries, EPC409-09 v8.0  (European Payments Council) · VIES availability  (VerifNow, read 20 September 2026) · VAT Directive 2006/112/EC, Article 138  (EUR-Lex)

Last updated on