Skip to Content
B2B Signup Forms

Validating a B2B signup form: VAT number, work email and phone

A B2B signup form usually asks for three things a consumer form does not: a VAT number, a work email, and a phone number. Each one can be checked. The hard part is not the checking — it is deciding what a failed check should do.

The mistake is treating every check as a gate. Each field fails in ways that say nothing about whether the person is a legitimate customer: a registry that is down, a company whose only mailbox is contact@, a business that runs on VoIP. Block on those and you lose real signups to your own validation.

This page gives one rule per field, then the code.

The rule across all three: reject what is impossible, flag what is unusual, and accept what you merely could not confirm. An impossible value — a VAT number with the wrong structure, a domain that receives no mail — will never become valid. Everything else is information for you to use, not a verdict to show the user.


The decision table

FieldRejectAsk the user to confirmAccept, and record
VAT numberWrong structure for its country · Registry says not registered—Registry unreachable (registered: null) · Answer served from cache
Work emailvalid: false — malformed, no mail records, blocked domain · Disposable, on a trialLikely typo (gmail.con)contact@ / info@ · Consumer mailbox, if you allow freelancers
PhoneNot a number in its country’s plan · Premium-rateMissing country codeVoIP · Landline · Country different from the VAT number

The columns are deliberately uneven. Most signals belong on the right.


VAT number

A VAT number answers two separate questions, and only one of them is always answerable.

Is it shaped like a real VAT number? Decided locally, from each member state’s rules. If not, the number will never be valid — reject it, with a message that says so.

Is it registered? Only VIES, the European Commission’s registry, can say — and VIES is a federation of 27 national systems with no SLA. We measured it: some member states were unreachable for more than a day in total over nine days (the data).

When VIES cannot be reached, VerifNow returns registered: null, not false. Accept the signup. A number whose registration you could not check is not an unregistered number, and rejecting it turns a registry outage into a lost customer.

const vat = vatResult.vatDetails; if (!vat.formatValid) return fieldError('vat', 'This VAT number is not correctly formed.'); if (vat.registered === false) return fieldError('vat', 'This VAT number is not registered in VIES.'); // registered === null: VIES was unreachable. Accept, and check again later. account.vatVerified = vat.registered === true; account.vatSource = vat.source; // LIVE, CACHE, STALE or UNVERIFIED

If the customer will be invoiced under reverse charge, keep the evidence of the check: source, checkedAt, and — when your own account has a VAT number configured — consultationNumber, the receipt VIES issues to an identified requester. See Validate VAT.


Work email

Reject what valid: false rejects

valid: false rests on hard facts only: empty or malformed input, a domain with neither MX nor A records, or a blocked domain. That is worth rejecting, with the message the API returns — and the verdict is the same on every plan.

Everything else is a signal. deliverability: UNDELIVERABLE in particular does not mean valid: false: a likely typo at a real domain can receive mail, just not the mail your user meant to receive. Decide on those yourself, as below. See Validate Email.

Ask about typos — even when valid is true

typo_detected is set when the domain looks like a misspelling of a common one. A typo domain can still have mail records, so do not wait for valid: false: show the suggestion.

const { typoDetected, suggestedDomain } = emailResult.emailDetails.signals; if (typoDetected) { const [local] = email.split('@'); return fieldHint('email', `Did you mean ${local}@${suggestedDomain}?`); }

Accept contact@ and info@

Role addresses are routine in B2B. For a small company, contact@ is often the only mailbox it has. VerifNow reports them in role_based and they raise the risk score, but on their own they do not make an address invalid. Blocking them should be a deliberate decision about your market, not a default.

Decide on consumer mailboxes — don’t let a library decide

Whether to accept @gmail.com on a B2B form is a business question. Freelancers and very small companies use consumer mailboxes; enterprise-only products may not want them.

free_provider answers it, but it is only returned on the GROWTH and PRO plans. On FREE and STARTER the field is absent, not false — so check it exists before branching on it:

const { freeProvider } = emailResult.emailDetails.signals; if (freeProvider === undefined) { // Not computed at this plan's depth. Don't treat this as "not a free provider". } else if (freeProvider) { account.flags.push('consumer-mailbox'); }

Disposable addresses are the one signal worth considering a hard block on a B2B trial, since a throwaway inbox is rarely a buyer. disposable is returned on every plan.


Phone

VerifNow validates a number against its country’s numbering plan: the right length, and a prefix that is actually allocated. It needs the country code — +33 6 12 34 56 78, or 0033… — because without one the same digits are valid in one country and nonsense in another.

  • valid: false: reject, and show message. It says what is wrong (“too short for its country”, “must include its country code”).
  • line_type: "PREMIUM_RATE": reject. A business contact number is not a premium line.
  • VOIP and FIXED_LINE: accept. Plenty of companies run on VoIP.

A phone country that differs from the VAT country is normal — subsidiaries, remote teams, founders abroad. Record it; do not reject it.

Valid means the number exists in the plan, not that it rings. If you need to reach the person, send a code. See Validate Phone.


Putting it together

One API key covers the three fields. There is no batch endpoint: send three requests in parallel.

import { VerifNow, VerifNowRateLimitError } from '@verifnow/sdk'; const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY! }); export async function checkSignup(form: { vat: string; email: string; phone: string }) { const [vat, email, phone] = await Promise.allSettled([ client.validateVat(form.vat), client.validateEmail(form.email), client.validatePhone(form.phone), ]); const errors: Record<string, string> = {}; const flags: string[] = []; // VAT if (vat.status === 'fulfilled') { const d = vat.value.vatDetails!; if (!d.formatValid) errors.vat = 'This VAT number is not correctly formed.'; else if (d.registered === false) errors.vat = 'This VAT number is not registered in VIES.'; else if (d.registered === null) flags.push('vat-unverified'); } else { flags.push('vat-unchecked'); } // Email if (email.status === 'fulfilled') { const { valid, message, emailDetails } = email.value; const s = emailDetails?.signals; if (s?.typoDetected) errors.email = `Did you mean ${form.email.split('@')[0]}@${s.suggestedDomain}?`; else if (!valid) errors.email = message ?? 'This email address cannot receive mail.'; else if (s?.disposable) errors.email = 'Please use your work email address.'; if (s?.freeProvider) flags.push('consumer-mailbox'); } else { flags.push('email-unchecked'); } // Phone if (phone.status === 'fulfilled') { const { valid, message, phoneDetails } = phone.value; if (!valid) errors.phone = message ?? 'This phone number is not valid.'; else if (phoneDetails?.lineType === 'PREMIUM_RATE') errors.phone = 'Please enter a standard-rate number.'; } else { flags.push('phone-unchecked'); } // A 429 that survived the SDK's retries — usually a spent quota — is not a reason to accept // silently. Surface it to whoever operates the form. for (const r of [vat, email, phone]) { if (r.status === 'rejected' && r.reason instanceof VerifNowRateLimitError) throw r.reason; } return { ok: Object.keys(errors).length === 0, errors, flags }; }

Three choices in there are worth making consciously:

  • Promise.allSettled, not Promise.all. One failed call should not discard the other two answers.
  • A network failure accepts the field and flags it (vat-unchecked). The SDK throws instead of returning valid: true on failure precisely so that this is your decision. The opposite choice — blocking signups while the validation service is unreachable — is defensible too; make it on purpose.
  • A spent quota is re-thrown. Accepting every signup unverified until the end of the month is the kind of failure nobody notices.

The code needs @verifnow/sdk 1.2.0 or later, which models phoneDetails and consultationNumber. The SDK maps the API’s snake_case fields to camelCase.


What it costs, and what it needs

Each field is one validation, so one signup is three validations:

PlanValidations per monthSignups per monthRequests in flightSignups checked simultaneously
FREE250~8331
STARTER10,000~3,30051
GROWTH50,000~16,700103
PRO150,00050,000258

The concurrency limit counts requests in flight, not signups per second. The three calls of a signup usually finish well under a second; a VAT check waiting on a slow VIES can take up to four. A request over the limit gets a 429, which the SDK retries with a short backoff. See Rate Limits.

Signals by plan: FREE and STARTER run the same checks — they differ in volume only. GROWTH adds free_provider, risk_level and domain age for email. VAT and phone checks are identical on every plan.


What not to validate

  • Don’t require a mobile number unless you actually send SMS. Landlines and VoIP are how many businesses answer.
  • Don’t cross-check countries as a gate. A Belgian company with a French phone number and an Irish VAT registration is unusual, not fraudulent.
  • Don’t validate on every keystroke. Each call counts against your quota. Validate when the field loses focus, and reuse that answer on submit unless the value changed.
Last updated on