Validate Belgian VAT Numbers (BTW/TVA) in Node.js

28 Sep 2026

Validate Belgian VAT Numbers (BTW/TVA) in Node.js

Belgium sits at the center of a lot of EU B2B trade, and its VAT number is simpler than most: there is no separate VAT-specific sequence to confuse with a company number, no sole-trader carve-out like the Dutch btw-id, and only one register to reason about. The format check is trivial. The part worth getting right is what the Belgian company register can and cannot tell you when VIES is unavailable. Here is the full pipeline in Node.js – this post targets the Node.js/JavaScript implementation specifically; for the general EU-wide behavior of the /vat-api/be endpoint, see the country reference.

What a Belgian VAT number looks like

A Belgian VAT ID has a short, fixed shape:

  • BE — country code
  • 10 digits – the body always starts with 0 or 1

So BE0123456794 is BE followed by a 10-digit body whose first digit is 0 or 1. You will also see it written as BTW (the Dutch acronym, Belasting over de Toegevoegde Waarde) or TVA (the French acronym, Taxe sur la Valeur Ajoutée) – same number, different label depending on which language the business operates in.

The detail that actually matters for how you build the validator: the Belgian VAT number is the KBO/CBE enterprise number itself, with a BE prefix. KBO (Kruispuntbank van Ondernemingen, Dutch) and CBE (Banque-Carrefour des Entreprises, French) are the two names for the same national enterprise register. Every registered Belgian entity gets one enterprise number; VAT registration does not create a second identifier – it ‘activates’ the existing one for VAT purposes. That is a meaningfully simpler shape than a country that runs the VAT number and the company-registry number as two different sequences: in Belgium, one number does both jobs, and you never have to bridge between them.

Format validation in Node.js

Reject malformed input before any network call. As with the sibling guides in this series, decide up front which mode you’re running in:

  • Strict (API mode) – your service exposes a documented BE + 10-digit format. Reject anything else.
  • Lenient (checkout mode) – accept what customers type and try to make it work. Spaces, dots, and a missing BE prefix are common; auto-correct them and log the original input.
const BE_VAT_PATTERN = /^BE[01]\d{9}$/

function normaliseBelgianVatId(
  input: string,
  opts: { mode?: 'strict' | 'lenient' } = {}
): string | null {
  let cleaned = input.replace(/[\s.\-]/g, '').toUpperCase()
  // Lenient mode: prepend BE when the user typed only the 10-digit body
  if (opts.mode === 'lenient' && /^[01]\d{9}$/.test(cleaned)) {
    cleaned = `BE${cleaned}`
  }
  return BE_VAT_PATTERN.test(cleaned) ? cleaned : null
}

// Strict (default) — API consumers should send a fully-qualified ID
normaliseBelgianVatId('BE 0123.456.794') // → 'BE0123456794'
normaliseBelgianVatId('0123456794') // → null

// Lenient — for checkout forms
normaliseBelgianVatId('0123456794', { mode: 'lenient' }) // → 'BE0123456794'
normaliseBelgianVatId('BE212345679', { mode: 'lenient' }) // → null (11 digits, wrong length)

Belgium also has a real check digit, unlike the Netherlands’ post-2020 sole-trader numbers, which is worth using as a soft pre-filter. The rule: the last two digits of the 10-digit body equal 97 − (first eight digits mod 97).

// Soft signal only — a pass does NOT mean the number is registered.
// Worked example: body 12345678xx → 12345678 % 97 = 3 → 97 - 3 = 94 → BE0123456794.
function hasValidBelgianCheckDigits(vatId: string): boolean | null {
  const match = /^BE([01]\d{9})$/.exec(vatId)
  if (!match) return null
  const body = match[1]
  const first8 = Number(body.slice(0, 8))
  const check = Number(body.slice(8, 10))
  const remainder = first8 % 97
  const expected = remainder === 0 ? 97 : 97 - remainder
  return check === expected
}

hasValidBelgianCheckDigits('BE0123456794') // → true
hasValidBelgianCheckDigits('BE0123456700') // → false

The remainder === 0 branch above (expecting 97 rather than 00) is the conventional treatment described in most references on this checksum, but sources are not unanimous on that edge case – do not hard-fail a lookup on it locally. Treat the whole check-digit function the same way you’d treat any local checksum: a cheap way to catch a fat-fingered digit before you spend a network call, never a substitute for asking VIES.

A regex and a check digit prove the string is well-formed. Neither proves the entity is currently VAT-registered – that answer only comes from VIES.

Calling VIES for Belgium

VIES routes a BE query to the Belgian tax administration and reports back what it says. For a live check you get back one of:

  • A valid/invalid answer, with trader name and address when the number is valid and the trader has not opted out of disclosure – treat name/address as commonly present for Belgium, not guaranteed on every request.
  • MS_UNAVAILABLE — the Belgian node is temporarily unreachable.
  • SERVICE_UNAVAILABLE — VIES itself is degraded.
  • A timeout after roughly 10 seconds.

No national node is online 100% of the time. The VIES downtime guide covers the failure modes worth designing for across every member state, and the pattern for Belgium is the same as anywhere else: MS_UNAVAILABLE is not ‘invalid,’ and blocking checkout on it is a self-inflicted wound.

CBE/KBO enrichment: what it adds, and what it doesn’t

vatnode runs a live integration against the CBE/KBO public register, and it is worth being precise about what that integration does and does not do.

What it adds. When VIES confirms a Belgian number is valid, vatnode enriches the result with data VIES itself does not return: the company’s legal form (e.g. Public limited company (NV)), its registration date, a NACE-based activity description, and the registry code under the label Ondernemingsnummer – the enterprise number by its Belgian name. This is the same kind of enrichment covered in what a VAT check returns beyond valid or invalid: a richer profile riding on top of a validity answer VIES already gave you.

What it explicitly does not do: answer valid or invalid when VIES BE is unavailable. Presence in the enterprise register is not the same claim as VAT registration. A Belgian entity can be listed and active in CBE/KBO without being registered for VAT – below a registration threshold, exempt, or deregistered for VAT while remaining active as a business. Deriving a valid/invalid verdict from register presence during a VIES outage would produce false positives: entities that look fine in the company register but are not actually VAT-registered. So when VIES BE is down, vatnode surfaces the VIES error rather than guessing from CBE data – the enrichment fields simply stay empty until VIES answers again. This mirrors how vatnode treats company/enterprise registers generally across the countries where it has one: useful for enrichment, never substituted for the VAT register itself. See the full country coverage for which sources back which country.

That distinction is the one thing to get right in your own integration too: build enrichment as an optional, best-effort layer on a VIES-confirmed result, not as a fallback path for the yes/no question.

Full working example with the vatnode API

vatnode runs format normalization and the requester-qualified VIES call to the Belgian node, then layers CBE enrichment on top when VIES answers. One call:

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

if (!res.ok) {
  // Branch on the error code — see the error table below.
  const { error } = await res.json()
  throw new Error(`VAT check failed: ${error.code}`)
}

const data = await res.json()
// {
//   "valid": true,
//   "vatId": "BE0123456794",
//   "countryCode": "BE",
//   "countryName": "Belgium",
//   "companyName": "Example NV",
//   "companyAddress": "Voorbeeldstraat 1, 1000 Brussel",
//   "companyRegistrationDate": "2009-06-12",
//   "companyForm": "Public limited company (NV)",
//   "industryDescription": "Computer programming activities",
//   "registryCode": "0123456794",
//   "registryCodeName": "Ondernemingsnummer",
//   "source": "VIES",
//   "consultationNumber": "WAPIAAAAX9999999",
//   "checkId": "019d2a89-a5d9-7b97-b710-57b84604de2b",
//   "verifiedAt": "2026-08-28T08:30:00.000Z"
// }

Verify BE0123456794 yourself via VIES before treating it as a live example – this article is not asserting it belongs to any specific company. Notice source stays "VIES" here: unlike a country with an active national fallback, a Belgian response never carries a different source value for CBE data – the enterprise-register fields are enrichment riding on the VIES verdict, not an alternate source of truth. If companyForm, industryDescription, or registryCode come back null, VIES answered but CBE enrichment did not – treat that as a gap in the extra data, not a problem with the validation itself. Field-by-field documentation lives in the API reference, and the Node.js VAT validation guide covers the storage schema vatnode customers use across countries. If you’re weighing building this pipeline yourself against using a provider, build vs buy: VAT validation via VIES or an API walks through the trade-off in detail.

Error handling

Treat ‘we could not get an answer’ as a distinct state from ‘invalid.’ The vatnode API returns explicit, machine-readable error codes so you can branch correctly:

  • INVALID_FORMAT (400) – the string is not a well-formed VAT ID. Client mistake; surface it inline.
  • INVALID_REQUESTER (422) – the requester VAT ID used for the qualified lookup was rejected.
  • RATE_LIMITED (429) – back off and retry.
  • VIES_UNAVAILABLE (503) – the Belgian node is down. Transient; queue a retry, do not treat as invalid.
  • VIES_ERROR (502) – VIES answered with an error. Transient.
  • UPSTREAM_TIMEOUT (504) – the upstream took too long. Transient.
  • INTERNAL_ERROR (500) – retry, then alert.

Because Belgium has no validity fallback – CBE is enrichment-only, as covered above – a VIES_UNAVAILABLE response for a BE number is a genuine dead end until VIES recovers, not a signal to check somewhere else and guess. Queue it for retry rather than blocking checkout: the customer almost certainly gave you a real number, and you simply cannot confirm it yet. The VIES error codes guide covers the full retry strategy for every code, and the audit trail guide covers what to persist once VIES does answer – consultationNumber in particular, since that is the evidence an auditor may ask for on an intra-EU reverse-charge supply.

FAQ

What is a Belgian VAT number?

It is the country code BE followed by the 10-digit KBO/CBE enterprise number (Ondernemingsnummer in Dutch, numéro d’entreprise in French). Belgium does not issue a separate VAT-specific sequence – the enterprise number itself is ‘activated’ for VAT when the entity registers.

Is the Belgian VAT number the same as the KBO/CBE enterprise number?

Yes. Unlike some member states that issue a distinct VAT identifier alongside a company number, Belgium’s BTW/TVA number is the enterprise number with a BE prefix – there is no second number to keep track of.

Does vatnode use the CBE register to validate a Belgian VAT number when VIES is down?

No. CBE/KBO enrichment adds company data – name, legal form, registration date, activity – to a VIES-confirmed result. It is never used to answer valid or invalid on its own, because a company can be listed and active in the enterprise register without being VAT-registered.

What is the check-digit rule for a Belgian VAT number?

The last two digits of the 10-digit body equal 97 minus the remainder of the first eight digits divided by 97. It is a cheap local pre-filter for typos, not proof that the number is registered – only VIES confirms that.

This is general information about EU VAT and VIES, not tax or compliance advice – confirm the treatment of your own case with a qualified adviser.

Validate BE VAT numbers with company data in one call

vatnode runs the requester-qualified VIES check against the Belgian node and layers CBE/KBO company data on top when VIES confirms a match, with a stable response shape and the consultation number for your audit trail. Free plan, 100 requests/month.

Get a free API key · API reference · Belgian VAT API reference