Skip to content

IBAN Validator — TypeScript source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure IBAN validation logic - no React, no DOM, deterministic.
// Normalizes (uppercase, strip spaces/dashes), checks structure, validates the
// per-country length, and runs the ISO 13616 mod-97 checksum via BigInt.
// Never throws.

export interface IbanInfo {
  input: string;
  cleaned: string; // uppercase, spaces/dashes removed
  countryCode: string | null; // 2 uppercase letters
  valid: boolean; // structure + checksum + length all ok
  checksumOk: boolean; // mod-97 === 1
  lengthOk: boolean; // length matches country table
  expectedLength: number | null;
  formatted: string; // groups of 4
  error: string | null; // human reason when !valid
}

// Per-country IBAN lengths (ISO 13616) - a representative subset.
export const IBAN_LENGTHS: Record<string, number> = {
  AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BY: 28, BE: 16, BA: 20, BR: 29,
  BG: 22, CR: 22, HR: 21, CY: 28, CZ: 24, DK: 18, DO: 28, EE: 20, FO: 18,
  FI: 18, FR: 27, GE: 22, DE: 22, GI: 23, GR: 27, GL: 18, GT: 28, HU: 28,
  IS: 26, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, XK: 20, KW: 30, LV: 21,
  LB: 28, LI: 21, LT: 20, LU: 20, MK: 19, MT: 31, MR: 27, MU: 30, MC: 27,
  MD: 24, ME: 22, NL: 18, NO: 15, PK: 24, PS: 29, PL: 28, PT: 25, QA: 29,
  RO: 24, LC: 32, SM: 27, ST: 25, SA: 24, RS: 22, SC: 31, SK: 24, SI: 19,
  SG: 19, ES: 24, SE: 24, CH: 21, TL: 23, TN: 24, TR: 26, UA: 29, AE: 23,
  GB: 22, VG: 24,
};

/** mod-97 checksum over a CLEANED iban (no spaces, uppercase). */
export function mod97Check(cleaned: string): boolean {
  // Move the first 4 chars (country + check) to the end.
  const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
  let numeric = '';
  for (const ch of rearranged) {
    const code = ch.charCodeAt(0);
    if (code >= 48 && code <= 57) {
      numeric += ch;
    } else if (code >= 65 && code <= 90) {
      numeric += String(code - 55); // A=10 .. Z=35
    } else {
      return false; // invalid character
    }
  }
  // BigInt handles arbitrarily long IBANs.
  let rem = 0n;
  for (const ch of numeric) {
    rem = (rem * 10n + BigInt(ch.charCodeAt(0) - 48)) % 97n;
  }
  return rem === 1n;
}

/** Validate an IBAN. Always returns IbanInfo; never throws. */
export function validateIban(input: string): IbanInfo {
  const cleaned = (input || '').toUpperCase().replace(/[\s-]/g, '');
  const cc = /^[A-Z]{2}/.test(cleaned) ? cleaned.slice(0, 2) : null;

  const base: IbanInfo = {
    input: input ?? '',
    cleaned,
    countryCode: cc,
    valid: false,
    checksumOk: false,
    lengthOk: false,
    expectedLength: cc ? IBAN_LENGTHS[cc] ?? null : null,
    formatted: cleaned.replace(/(.{4})(?=.)/g, '$1 ').trim(),
    error: null,
  };

  if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/.test(cleaned)) {
    return { ...base, error: 'Invalid IBAN format.' };
  }

  const lengthOk = base.expectedLength === null ? true : cleaned.length === base.expectedLength;
  const checksumOk = mod97Check(cleaned);

  if (!lengthOk) {
    return { ...base, lengthOk: false, checksumOk, valid: false, error: `Length should be ${base.expectedLength} for ${cc}.` };
  }
  if (!checksumOk) {
    return { ...base, lengthOk: true, checksumOk: false, valid: false, error: 'Checksum failed.' };
  }
  return { ...base, lengthOk: true, checksumOk: true, valid: true, error: null };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →