How to Generate Checksum-Valid Test IBANs in Code (Python, JS, PHP)

How to Generate Checksum-Valid Test IBANs in Code (Python, JS, PHP)

A practical guide to generating MOD-97-correct synthetic IBAN values in Python, JavaScript, and PHP, with the country-pattern limits made explicit.

Written by David S · Published on 2026-05-26 · Updated on 2026-07-18
#generate IBAN #IBAN in code #Python IBAN #JavaScript IBAN #PHP IBAN #test data #MOD-97 #fixtures

At some point, most developers working on payments hit the same wall: they need checksum-valid IBAN-shaped values for tests, and there is no good way to type them by hand. Copy-pasting the same handful of examples leads to brittle suites, and using customer account data creates unnecessary privacy and operational risk. A safer approach is to generate synthetic values programmatically and keep them isolated from live payment systems.

This guide walks through exactly how to do that in Python, JavaScript, and PHP. By the end you will have a small, dependency-light function you can drop into a factory, a seed script, or a fixtures module. If you just need a handful of numbers right now without writing code, the Random IBAN generator produces them instantly and exports to CSV, JSON, or TXT.


What "Generating an IBAN" Actually Means

An IBAN is not a single opaque number — it is three concatenated parts:

ES 76 21000418450200051332
│  │  └── BBAN (country-specific: bank + branch + account)
│  └───── Check digits (calculated, positions 3–4)
└──────── Country code (ISO 3166-1, positions 1–2)

So generating one means doing three things in order:

  1. Pick a country and look up its IBAN length and BBAN layout.
  2. Build a random BBAN that matches that layout (the right number of digits, and letters where the spec allows them).
  3. Calculate the two check digits with the MOD-97 algorithm so the whole thing validates.

The first step needs a small lookup table. The second is plain random generation. The third is the only part with real logic, and it is the same routine you would use to validate an IBAN, run in reverse.


The Core: Calculating Check Digits

Every language below shares the same check-digit routine. The rule from ISO 7064 / ISO 13616 is:

  1. Take the BBAN, append the country code, then append 00.
  2. Replace each letter with two digits (A=10, B=11, … Z=35).
  3. Read the result as one big integer and compute 98 - (integer mod 97).
  4. Zero-pad that result to two digits.

That padded number is your check-digit pair. Drop it between the country code and the BBAN, and the finished IBAN passes MOD-97 validation. Let's implement it.


Generating an IBAN in Python

Python's arbitrary-precision integers make this almost trivial — no big-integer library required.

import random

# Minimal layout table: country -> (total length, BBAN length)
IBAN_SPECS = {
    "DE": 22, "ES": 24, "FR": 27, "GB": 22,
    "NL": 18, "IT": 27, "PT": 25, "BE": 16,
}

def _check_digits(country: str, bban: str) -> str:
    rearranged = bban + country + "00"
    numeric = "".join(
        str(ord(c) - 55) if c.isalpha() else c
        for c in rearranged
    )
    check = 98 - (int(numeric) % 97)
    return f"{check:02d}"

def generate_iban(country: str = "ES") -> str:
    total_len = IBAN_SPECS[country]
    bban_len = total_len - 4            # minus country code + check digits
    bban = "".join(random.choices("0123456789", k=bban_len))
    return country + _check_digits(country, bban) + bban

# Usage
print(generate_iban("DE"))   # e.g. DE21 ... (valid checksum)
print([generate_iban("ES") for _ in range(5)])

This deliberately minimal example models only total length and MOD-97. Its all-numeric BBAN does not satisfy every listed country's domestic character pattern or national checksum rules (the UK, Netherlands, and Italy require letters in specific positions, for example), and a random bank identifier is not necessarily assigned or current. Treat the output as a checksum exercise. For country-format fixtures, encode the full SWIFT registry pattern and any domestic check-digit rules, or use the RandomIBAN implementation with its documented limits.


Generating an IBAN in JavaScript

The only wrinkle in JS is that the rearranged number is too large for a normal Number. The standard fix is a running-remainder loop, which keeps the value under 97 the whole time.

const IBAN_SPECS = {
  DE: 22, ES: 24, FR: 27, GB: 22,
  NL: 18, IT: 27, PT: 25, BE: 16,
};

function mod97(numericString) {
  let remainder = 0;
  for (const ch of numericString) {
    remainder = (remainder * 10 + Number(ch)) % 97;
  }
  return remainder;
}

function checkDigits(country, bban) {
  const rearranged = bban + country + '00';
  const numeric = rearranged.replace(/[A-Z]/g, c =>
    (c.charCodeAt(0) - 55).toString()
  );
  const check = 98 - mod97(numeric);
  return String(check).padStart(2, '0');
}

function generateIban(country = 'ES') {
  const bbanLen = IBAN_SPECS[country] - 4;
  let bban = '';
  for (let i = 0; i < bbanLen; i++) {
    bban += Math.floor(Math.random() * 10);
  }
  return country + checkDigits(country, bban) + bban;
}

// Usage
console.log(generateIban('FR'));
console.log(Array.from({ length: 5 }, () => generateIban('NL')));

If you need cryptographically stronger randomness (rarely necessary for test data), swap Math.random() for crypto.getRandomValues(). For fixtures and demos, the simple version is perfectly adequate.


Generating an IBAN in PHP

This PHP example needs the optional bcmath extension for the large modulo. Make sure it is installed and enabled in the environment where you run the code.

<?php
const IBAN_SPECS = [
    'DE' => 22, 'ES' => 24, 'FR' => 27, 'GB' => 22,
    'NL' => 18, 'IT' => 27, 'PT' => 25, 'BE' => 16,
];

function checkDigits(string $country, string $bban): string {
    $rearranged = $bban . $country . '00';
    $numeric = '';
    foreach (str_split($rearranged) as $ch) {
        $numeric .= ctype_alpha($ch) ? (string)(ord($ch) - 55) : $ch;
    }
    $check = 98 - (int) bcmod($numeric, '97');
    return str_pad((string) $check, 2, '0', STR_PAD_LEFT);
}

function generateIban(string $country = 'ES'): string {
    $bbanLen = IBAN_SPECS[$country] - 4;
    $bban = '';
    for ($i = 0; $i < $bbanLen; $i++) {
        $bban .= random_int(0, 9);
    }
    return $country . checkDigits($country, $bban) . $bban;
}

// Usage
echo generateIban('IT'), PHP_EOL;

Note the use of random_int() rather than rand() — it is the modern, unbiased choice and costs nothing extra here.


Using Generated IBANs in a Test Suite

Once you have a generator, wire it into your tests rather than scattering literals. A few patterns that work well:

  • Factories and fixtures. Call generateIban() inside your factory (FactoryBoy, Faker provider, Laravel factory) so every test run gets fresh checksum fixtures.
  • Seed data for staging. Populate demo databases with a few hundred IBANs across several countries to exercise country-specific formatting.
  • Boundary cases. Keep a separate set of deliberately broken strings (wrong length, bad checksum, unsupported country) to test that your validation rejects them.
  • Determinism when you need it. Seed your RNG at the start of a test if you want reproducible IBANs; leave it unseeded for fuzz-style coverage.

Whatever you generate, label it clearly as synthetic. These numbers can pass configured structure and MOD-97 checks, but RandomIBAN does not verify whether a generated value coincides with an assigned account. They must never reach a production payment rail. See our guide on storing IBANs safely in payment systems for how to keep test and production data from mixing.


When Not to Write Your Own Generator

Rolling your own is great for tight integration, but it is not always worth it. Skip the code when:

  • You need a quick batch right now — paste them in seconds from the Random IBAN generator and export to CSV/JSON.
  • You need broad country coverage with correct BBAN sub-structures (national check digits, letter positions) that a minimal table does not capture.
  • You are working outside a codebase — preparing a spreadsheet, a Postman collection, or documentation.

And whichever route you take, validate the output. Run a sample through the IBAN validator to confirm your check-digit logic matches the reference implementation. If anything fails MOD-97, the bug is almost always in the letter-to-number conversion or an off-by-one in the BBAN length.


FAQ

Are programmatically generated IBANs safe to use?

They are suitable for isolated format validation, QA, demos, and documentation when they are clearly labelled and blocked from live payment rails. Passing structure and checksum checks does not prove that a value is unassigned, so never submit generated values to a real transaction or production payment system.

Do I need an external library to generate an IBAN?

The Python and JavaScript examples need no external package. The PHP example requires the optional bcmath extension to be installed and enabled. A dedicated library or additional country rules may still be useful when you need complete national BBAN patterns rather than only length and MOD-97 handling.

Why is my generated IBAN failing validation?

The most common causes are an incorrect letter-to-number mapping (it must be A=10 through Z=35), forgetting to append the country code and 00 before the modulo step, or using the wrong BBAN length for the country. Compare a single failing case against the validator to isolate it.

How do I generate IBANs for countries with letters in the BBAN?

Extend the random-BBAN step to place letters where the country's spec allows them — for example, the UK bank code is four letters. Keep a per-country pattern (which positions are letters versus digits) instead of a single length, then fill each position accordingly before calculating the check digits.

Can I generate thousands of IBANs at once?

Yes. The generator functions are cheap to call in a loop, so producing thousands is instant. If you would rather not run code, the Random IBAN generator supports bulk generation and one-click export to CSV, JSON, and TXT.

What is the difference between generating and validating an IBAN?

Checksum validation tests an existing value by confirming mod 97 == 1. Generation runs the same arithmetic in reverse: you build the BBAN first, then compute 98 - (mod 97) to find check digits that make the value pass MOD-97. They share the same core routine; neither step proves that the country-specific BBAN is complete or that an account exists.

Try Our IBAN Tools

Put your knowledge into practice with our free tools.