Validate Irish VAT Numbers in Node.js

2 Oct 2026

Validate Irish VAT Numbers in Node.js

Ireland has issued VAT numbers in three different shapes over the years. The Irish VAT number format you’ll meet today is IE + 7 digits + a check letter + a second letter, but a shorter version without that second letter, and an older legacy shape with a letter buried in the middle of the digits, are both still valid. Here’s how to tell them apart in code, how the mod-23 check character works across all three, and why a checksum-valid number is not the same thing as a registered one. This post covers the Node.js/JavaScript implementation; for the general EU-wide behavior of the endpoint, see the Ireland VAT API reference.

What an Irish VAT number looks like

Three shapes are live today, all under the IE prefix:

  • Current, since 1 January 2013: IE + 7 digits + check letter (A–W) + second letter. Revenue’s practice – not a published rule – is A for individuals and H for non-individuals: companies, trusts, partnerships.
  • Older, still valid: IE + 7 digits + check letter, with no second letter – issued before the current format.
  • Legacy, still valid if already held: IE + 1 digit + a letter, +, or * in the second position + 5 digits + check letter – 8 characters after IE, arranged differently from the other two.

So IE1234567WH, IE1234567T, and IE8D79739I are all legitimate shapes. Don’t reject one as malformed because it’s shorter or has a letter where you’d expect a digit – the current format is just the one you’ll see most often on new registrations. VAT number validation for Ireland also means handling that spread of shapes – for how countries differ on a single number space versus a separate VAT-specific sequence, see the full EU-wide format reference, or the Portuguese and German write-ups in this series for two more shapes of that split.

Northern Ireland uses a separate XI prefix – that’s a different number space entirely, not an Irish one, and out of scope here.

Format validation in Node.js

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

  • Strict (API mode) – your service documents IE + one of the three shapes above. Reject anything else.
  • Lenient (checkout mode) – accept what customers paste. Spaces, dots, and a missing IE prefix are common; normalize and log the original input.
// Matches all three live shapes: current (7 digits + check letter + optional second letter)
// and legacy (digit + letter/+/* + 5 digits + check letter)
const IE_VAT_PATTERN = /^IE\d{7}[A-W][AH]?$|^IE\d[A-Z+*]\d{5}[A-W]$/

function normaliseIrishVatId(
  input: string,
  opts: { mode?: 'strict' | 'lenient' } = {}
): string | null {
  let cleaned = input.replace(/[\s.\-]/g, '').toUpperCase()
  // Lenient mode: prepend IE when the user typed only the body (8 chars for old/legacy, 9 for current)
  if (opts.mode === 'lenient' && /^[0-9A-Z+*]{8,9}$/.test(cleaned)) {
    cleaned = `IE${cleaned}`
  }
  return IE_VAT_PATTERN.test(cleaned) ? cleaned : null
}

// Strict (default) — API consumers should send a fully-qualified ID
normaliseIrishVatId('IE 1234567 WH') // → 'IE1234567WH'
normaliseIrishVatId('1234567WH') // → null

// Lenient — for checkout forms
normaliseIrishVatId('1234567WH', { mode: 'lenient' }) // → 'IE1234567WH'
normaliseIrishVatId('8D79739I', { mode: 'lenient' }) // → 'IE8D79739I' (legacy shape)

A regex match proves the shape and nothing else – not even that the check letter is arithmetically correct.

The mod-23 check character

All three shapes share the same modulus-23 scheme, just with different digit arrangements feeding into it. Weight the 7 digits 8, 7, 6, 5, 4, 3, 2 left to right, sum the products, and if a second letter is present add 9 times that letter’s index in the alphabet WABCDEFGHIJKLMNOPQRSTUV (W = 0, A = 1, … V = 22). Take the total mod 23, and the check letter is the character at that index in the same alphabet.

const IE_CHECK_ALPHABET = 'WABCDEFGHIJKLMNOPQRSTUV'
const IE_CHECK_WEIGHTS = [8, 7, 6, 5, 4, 3, 2]

function ieCheckLetter(sevenDigits: string, secondLetter?: string): string {
  const digits = sevenDigits.split('').map(Number)
  let total = digits.reduce((acc, d, i) => acc + d * IE_CHECK_WEIGHTS[i], 0)
  if (secondLetter) {
    total += 9 * IE_CHECK_ALPHABET.indexOf(secondLetter)
  }
  return IE_CHECK_ALPHABET[total % 23]
}

// Worked example (synthetic, current format): digits 1234567, second letter H
// products: 1×8+2×7+3×6+4×5+5×4+6×3+7×2 = 112; plus 9×8 (H is index 8) = 72 → 184
// 184 mod 23 = 0 → 'W'
ieCheckLetter('1234567', 'H') // → 'W' → IE1234567WH

function hasValidIrishCheckLetter(vatId: string): boolean | null {
  const current = /^IE(\d{7})([A-W])([AH])?$/.exec(vatId)
  if (current) {
    return current[2] === ieCheckLetter(current[1], current[3])
  }
  // Legacy shape: IE + digit + letter/+/* + 5 digits + check letter
  // Convert to the same 7-digit-plus-check form before checking: '0' + chars[2..6] + chars[0]
  const legacy = /^IE(\d)([A-Z+*])(\d{5})([A-W])$/.exec(vatId)
  if (legacy) {
    const rearranged = `0${legacy[3]}${legacy[1]}`
    return legacy[4] === ieCheckLetter(rearranged)
  }
  return null
}

// Worked example (legacy format): IE8D79739I
// rearranged: '0' + '79739' + '8' = '0797398'
// products: 0×8+7×7+9×6+7×5+3×4+9×3+8×2 = 193 → 193 mod 23 = 9 → 'I'
hasValidIrishCheckLetter('IE8D79739I') // → true

IE1234567WH above is a synthetic number built purely to demonstrate the current-format arithmetic. Neither it nor IE8D79739I is an assertion that a real entity holds that number – don’t use either as a VIES test fixture.

The mod-23 check character is a local pre-filter, not proof of registration. A checksum-valid IE number can still come back invalid from VIES – never issued, deregistered, or simply wrong. Run the checksum to catch a mistyped character before you spend a network call; run VIES to get the answer that actually matters.

An Irish VAT number is not the CRO number

Revenue issues a Tax Reference Number (TRN) when a business registers for tax. The CRO (Companies Registration Office) number is a different identifier, issued by a different body, at a different step: incorporation. A company gets a CRO number on incorporation, and a TRN and a VAT number separately when it registers for tax. For a sole trader, the TRN is the same as their PPSN; that’s a Revenue-specific equivalence and has nothing to do with the CRO.

In practice this means: don’t store a CRO number in a VAT-number field, and don’t validate a CRO number as if it were a VAT ID or vice versa. If an onboarding form asks for a ‘company registration number,’ keep it in its own field rather than folding it into whatever you pass to a VAT check.

Calling VIES for Ireland

VIES routes an IE query to Irish Revenue and relays back what it says. For a live check you get one of:

  • A valid/invalid answer, currently with trader name and address attached – treat this as typically present for Ireland, not guaranteed on every request.
  • MS_UNAVAILABLE — the Irish node is temporarily unreachable.
  • SERVICE_UNAVAILABLE — VIES itself is degraded.
  • A timeout after roughly 10 seconds.

None of that is unique to Irish numbers – every member-state node can go down. The VIES downtime guide covers the failure modes worth designing around across all of them. What’s distinct about an Irish check is what happens next when the node doesn’t answer.

Why Ireland has no national fallback or enrichment

For some member states, when the VIES node goes down, vatnode can fall back to a national tax authority or company registry to keep answering, and separately enrich a valid result with extra company data pulled from a registry. Ireland has neither. There is no national fallback source wired into vatnode’s pipeline for IE numbers, and there is no company-registry enrichment either – a result never carries a registryCode, registryCodeName, taxId, or taxIdName. Unlike some countries where the VAT body itself doubles as a published registry number that vatnode can echo back as a registryCode, an Irish VAT number isn’t the CRO number, so there’s nothing to derive one from.

If VIES IE is unavailable, the check simply reflects the outage – the same ‘VIES is the only authority for this country’s numbers’ reality the German guide walks through for a different member state. Current per-country coverage – which countries have a fallback, which have enrichment, which have neither – is documented at /docs/coverage rather than restated here.

If your business does meaningful volume with Irish counterparties, build checkout and onboarding to tolerate a VIES_UNAVAILABLE response gracefully – queue and retry, don’t block – rather than assuming a fallback will quietly cover the gap.

Full working example with the vatnode API

vatnode normalizes the format and runs a requester-qualified VIES call to the Irish node in one request:

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

if (!res.ok) {
  const { error } = await res.json()
  throw new Error(`VAT check failed: ${error.code}`)
}

const data = await res.json()
// {
//   "valid": true,
//   "vatId": "IE1234567WH",
//   "countryCode": "IE",
//   "countryName": "Ireland",
//   "companyName": "Example Ltd",         // typically present, not guaranteed
//   "companyAddress": "1 Example Street, Dublin", // same — typically present, not guaranteed
//   "registryCode": null,
//   "registryCodeName": null,
//   "taxId": null,
//   "taxIdName": null,
//   "source": "VIES",
//   "consultationNumber": "WAPIAAAAX...", // only when a requester VAT is set (dashboard → Account details)
//   "checkId": "019d2a89-a5d9-7b97-b710-57b84604de2b",
//   "verifiedAt": "2026-10-02T08:30:00.000Z"
// }

Verify IE1234567WH yourself via VIES before treating it as a live example – it’s a synthetic number chosen for the checksum walkthrough above, not an assertion that any specific entity holds it. source will always read "VIES" for an Irish check – there’s no IE fallback source to branch on. registryCode and taxId stay null: there’s no registry enrichment wired up for Ireland, so don’t build logic that expects them to populate.

If valid is false, that’s VIES saying the number isn’t currently registered for intra-EU VAT – a number can be valid domestically and still come back invalid from VIES. This article is informational, not tax advice, so confirm reverse-charge treatment with a qualified adviser rather than inferring it from the check alone. The consultationNumber is the VIES-issued reference covered in the audit trail guide; it’s returned only when your account has a requester VAT number set in dashboard Account details – useful documentary evidence, but not something only vatnode provides.

Get a free API key at vatnode.dev/register – 100 requests/month, no card – and the full field reference is in the API docs. The Node.js VAT validation guide covers the storage schema across every member state.

Error handling

Treat ‘we couldn’t get an answer’ as a distinct state from ‘invalid’ – this matters more for Ireland than for a country with a fallback, because there’s no second source to absorb a VIES IE outage. The vatnode API returns explicit error codes:

  • INVALID_FORMAT (400) – the string isn’t a well-formed IE VAT ID. Your input; surface it inline, don’t retry.
  • INVALID_REQUESTER (422) – your configured requester VAT number was rejected by VIES. Fix it in dashboard Account details.
  • RATE_LIMITED (429) – you’ve spent your quota. Retryable on a longer horizon.
  • VIES_UNAVAILABLE (503) – the Irish node (or VIES itself) is down. Transient – queue a retry, never mark the number invalid.
  • VIES_ERROR (502) / UPSTREAM_TIMEOUT (504) – transient upstream faults. Retry with backoff.
  • INTERNAL_ERROR (500) – retry, alert if it persists.

Because there’s no IE fallback to fill the gap, retry with exponential backoff (500ms, 2s, 5s) and, if it still fails, queue the check for a background re-run rather than blocking checkout. The full error taxonomy and retry table is in handling VIES error codes.

FAQ

What’s the difference between the old and new Irish VAT number format?

The new format, used since 1 January 2013, is IE followed by 7 digits, a check letter, and a second letter – Revenue’s practice, not a published rule, is to use A for individuals and H for companies, trusts and partnerships. The old format is the same but without that second letter – IE plus 7 digits and a check letter. Older still, there’s a legacy format with a letter or symbol in the second position of an 8-character body. Numbers already issued under the old or legacy formats remain valid.

Does a valid mod-23 check character mean an Irish VAT number is registered?

No. The check character only proves the digits are internally consistent – that nothing was mistyped. It’s a local pre-filter, not a registration check. Only a live VIES lookup confirms whether a number is currently registered for intra-EU VAT.

Is an Irish VAT number the same as a CRO company number?

No, and they come from two different bodies at two different steps. The Companies Registration Office issues a CRO number on incorporation. Revenue issues a Tax Reference Number when you register for tax, which is a separate step. For a sole trader, that TRN is the same as their PPSN. Don’t store a CRO number in a VAT field, and don’t validate one as if it were the other.

Why doesn’t vatnode have a national fallback or enrichment for Ireland?

Ireland is VIES-only in vatnode. There is no national tax-authority or company-registry integration wired into the fallback or enrichment path for IE numbers, so when VIES IE is unavailable there is no second source to consult – the check simply reflects the outage. See the current per-country coverage at /docs/coverage.

Validate Irish VAT numbers without owning the VIES IE retry logic

vatnode normalizes the input, runs a requester-qualified VIES call to the Irish node, and returns a stable response – with the consultation number for your audit trail when a requester VAT is configured. Free plan, 100 requests/month.

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