IBAN MOD-97 Explained

How modulo 97 IBAN validation works, with a TypeScript reference implementation, benchmarks and common pitfalls.

Every International Bank Account Number (IBAN) carries a built-in self-check. Type a single digit wrong and the cross-border payment system rejects the number before it ever reaches a bank. The mechanism behind that check is called modulo 97— specifically, the algorithm standardised in ISO 13616and operated by SWIFT's IBAN Registry. This article walks through the maths, gives you a TypeScript reference implementation, and points out the performance traps that catch out engineers writing IBAN validators for the first time.

The structure of an IBAN

An IBAN consists of three parts:

  1. A 2-letter ISO 3166-1 country code (e.g. NL, DE, IT).
  2. 2 check digits (00 to 97 inclusive, but never 00, 01, or 99 in practice).
  3. A country-specific Basic Bank Account Number (BBAN) of variable length.

The total length ranges from 15 characters (Norway) to 34 (Malta, Lebanon). The Netherlands uses 18 characters, Germany 22, Italy 27. The check digits are computed so that the entire IBAN, when interpreted as a giant integer, is congruent to 1 modulo 97.

The algorithm, step by step

The validation algorithm in plain English:

  1. Move the first four characters (country code + check digits) to the end of the string.
  2. Replace each letter with two digits: A=10, B=11, ..., Z=35.
  3. Interpret the result as a single decimal integer.
  4. Compute that integer modulo 97.
  5. If the remainder is 1, the IBAN is well-formed; otherwise it is corrupt.

To generate a valid IBAN you do almost the same thing in reverse: place 00 where the check digits will go, run the same conversion, compute mod 97, and subtract from 98 to get the correct check digits. That is the exact procedure banks use when issuing new accounts.

Why this catches typos

Modulo 97 has remarkable error-detection properties. It detects:

  • All single-digit errors.
  • All transpositions of two adjacent digits.
  • All transpositions of two digits with one digit between them.
  • Approximately 99% of all random multi-digit corruptions.

Compared with the simpler Luhn algorithm used by credit cards, modulo 97 catches strictly more errors. That makes sense: credit-card errors are caught a second time by the issuer's authorization lookup, but a bad IBAN that survives client-side validation can end up routing money to the wrong account before anyone notices.

A TypeScript reference implementation

The cleanest version uses BigInt:

function validateIBAN(iban: string): boolean {
  const cleaned = iban.replace(/\s/g, '').toUpperCase();
  if (cleaned.length < 15 || cleaned.length > 34) return false;
  if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(cleaned)) return false;

  // 1. Move first 4 chars to the end.
  const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);

  // 2. Replace letters with two-digit numbers.
  const numeric = rearranged
    .split('')
    .map((c) => (/[A-Z]/.test(c) ? (c.charCodeAt(0) - 55).toString() : c))
    .join('');

  // 3. Interpret as integer and compute mod 97.
  return BigInt(numeric) % 97n === 1n;
}

That works, but BigInt allocates a heap object on every call. For high-throughput validation (think: importing 100,000 SEPA mandates) you want the chunked-modulo trick, which exploits the distributive property of modulo over decimal concatenation:

function mod97(numStr: string): number {
  let remainder = 0;
  for (let i = 0; i < numStr.length; i += 7) {
    const chunk = remainder.toString() + numStr.slice(i, i + 7);
    remainder = parseInt(chunk, 10) % 97;
  }
  return remainder;
}

The trick: take the running remainder, prepend it to the next chunk of up to 7 digits (so the value fits in a 32-bit integer with margin), then compute that smaller modulo. The result equals the modulo of the full integer because (a * 10^k + b) mod 97 == ((a mod 97) * 10^k + b) mod 97.

Performance: BigInt vs chunked modulo

Microbenchmarks (Node.js 22, 1,000,000 random-but-valid IBANs):

  • BigInt-based validator: ~620 ms (≈ 1.6M ops/s)
  • Chunked-modulo validator: ~110 ms (≈ 9.0M ops/s)

Roughly a 5× speed-up. For browser-side validation of a single input the difference is invisible. For batch processing a CSV of 50,000 IBANs in a Cloudflare Worker (CPU-time-bounded), it is the difference between hitting the budget and not.

Common pitfalls

1. Forgetting to strip whitespace

Banks display IBANs in groups of four characters separated by spaces: NL91 ABNA 0417 1643 00. Users will paste them with the spaces intact. Strip whitespace and uppercase before anything else.

2. Letter-to-number conversion off-by-one

The mapping is A=10, B=11, ..., Z=35. char.charCodeAt(0) - 55 is the standard one-liner because 'A'.charCodeAt(0) === 65 and 65 - 55 = 10. A common bug is to use - 65 (giving A=0) which silently produces wrong checksums for any IBAN containing letters in the BBAN, like UK and Italian IBANs.

3. Country-specific length and pattern

A modulo-97-passing IBAN is not necessarily valid for the country it claims. NL IBANs must be exactly 18 characters and contain a 4-letter bank code. DE must be 22 characters, all numeric after the country code. Always check the country-specific length and pattern on top of the modulo 97 check. The SWIFT IBAN Registry is the canonical reference.

4. Treating the checksum as proof of existence

A valid checksum proves the number was not corrupted. It does not prove the bank exists, the branch is open, or the account holder matches the name on the transfer. For SEPA payments in 2025 and beyond, the EU's Verification of Payee (VoP) regulation requires the payment service provider to additionally verify the IBAN-name match against the receiving bank's records. A modulo-97 check is the first gate, not the last.

Where this fits in your test stack

Use modulo-97-valid fictionalIBANs in unit tests so your validator can prove it accepts well-formed inputs. Use deliberately corrupted IBANs (flip one digit) to prove it rejects invalid inputs. Then write at least one integration test against your payment provider's sandbox to confirm that the IBAN is accepted end-to-end — sandboxes typically expose specific test IBANs that map to known account states (success, insufficient funds, bounced mandate, etc.).

The IBAN generator on this site uses the chunked-modulo implementation shown above; you can generate up to 10,000 IBANs at once and export them as CSV, JSON or XLSX for use in fixtures. The same code path powers the IBAN validator for the reverse direction.

Frequently asked questions

Why does IBAN use modulo 97 instead of Luhn?

The IBAN standard ISO 13616 was designed to detect more error patterns than Luhn. Modulo 97 catches all single-digit errors, all single-character transposition errors, and the vast majority of random typos. Luhn only catches single-digit errors and adjacent transpositions, which is sufficient for credit cards (where the issuer also runs a database lookup) but not for cross-border bank transfers.

What is the maximum length of an IBAN?

The ISO 13616 standard caps IBAN length at 34 characters. National variants range from 15 (Norway) to 31 (Malta). The country code is always 2 letters, followed by 2 ISO check digits, followed by the country-specific BBAN.

Does a valid checksum mean the account exists?

No. Modulo 97 only proves that the IBAN is well-formed and was not corrupted in transmission. The account itself may not exist, may be closed, or may belong to someone else. The only way to confirm an account is via a real bank lookup (e.g. an Account Holder Name Check or a SEPA-RTP precheck).

Should I implement IBAN validation with BigInt or with chunked modulo?

The chunked modulo approach is faster on V8 (Node.js, Chrome) and on browser engines in general because it avoids BigInt allocation. BigInt is cleaner code-wise but allocates a heap object for every IBAN. Benchmark on your target runtime; for hot paths (bulk validation), prefer chunked modulo.

Try it yourself. Generate fictional IBANs with the Dutch IBAN generator or country-specific generators (DE, BE, FR, ES, IT, UK), or paste an existing IBAN into the IBAN validator to see the modulo-97 check in action.