If you are testing a SEPA transfer, a direct debit mandate, or any bank transfer integration, you need IBANs with correct check digits. This tool produces synthetic IBANs in each country’s own format, with valid ISO 7064 MOD-97-10 check digits, and validates ones you paste in. None of them is attached to a real account.
Test data only
IBAN Generator and Validator
Synthetic IBANs with correct ISO 7064 MOD-97-10 check digits, in each country's own format. Generated in your browser and linked to no account anywhere.
IBAN structure
An IBAN is three fields glued together:
TR33 0006 1005 1978 6457 8413 26
└┬┘ └┬┘ └──────────────┬───────┘
│ │ │
│ │ └── BBAN (Basic Bank Account Number) — country-specific
│ └──────────────────── Check digits (ISO 7064 MOD-97-10)
└──────────────────────── Country code (ISO 3166-1 alpha-2)
The country code and check digits are universal. Everything after them is the BBAN, and its internal structure is decided by each country’s banking authority — typically a bank code, sometimes a branch code, the domestic account number, and in several countries a national check digit of its own that predates the IBAN entirely.
That is the key thing to understand: the IBAN did not replace domestic account numbering. It wrapped it. A German IBAN contains the same Bankleitzahl and account number a German transfer always used; a British one contains the same sort code and account number. Only the outer layer is international.
How the IBAN check digits work
The algorithm is ISO 7064 MOD-97-10, and it runs in four steps:
- Move the first four characters — country code and check digits — to the end.
- Replace every letter with a number:
A= 10,B= 11, throughZ= 35. - Read the result as one very large integer and take it modulo 97.
- A valid IBAN leaves a remainder of exactly 1.
To produce check digits rather than verify them, put 00 in their place, run the same
calculation, and subtract the remainder from 98.
Why it is stronger than Luhn
Luhn catches single-digit errors and most adjacent transpositions. MOD-97-10 catches all single-character errors, all transpositions of adjacent characters, and the overwhelming majority of other common mistakes — the probability of a random error slipping through is about 1 in 97. That difference matters when a mistyped account number sends money to a stranger rather than just failing a form.
The comparison is worth holding onto when you work on both. A card number that fails Luhn validation is almost certainly mistyped, but a card number that passes tells you very little. An IBAN that passes MOD-97 is far stronger evidence that the characters are as the sender intended — still not evidence that the account exists, but a much narrower gap.
The BigInt trap
function isValidIban(iban) {
const s = iban.replace(/\s+/g, '').toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
const rearranged = s.slice(4) + s.slice(0, 4);
const numeric = rearranged.replace(/[A-Z]/g, (c) => c.charCodeAt(0) - 55);
return BigInt(numeric) % 97n === 1n;
}
console.log(isValidIban('GB82 WEST 1234 5698 7654 32')); // true
BigInt is not a stylistic choice here. Letters expand to two digits each, so a 31-character
Maltese IBAN becomes a 45-digit integer — around twenty digits past what a JavaScript
number represents exactly.
Here is the failure, using the published Maltese example MT84 MALT 0110 0001 2345 MTLC AST0 01S. After rearranging and substituting letters, the numeric string is 45 characters long:
Number(numeric) % 97returns 41BigInt(numeric) % 97nreturns 1
The first result is not an error. Nothing throws, nothing warns, and the function confidently
returns false for a perfectly valid IBAN. The symptom that gives it away is that short IBANs
validate correctly and long ones fail — which reads like a country-specific bug and sends
people looking in entirely the wrong place. The same applies in any language where the default
integer type is 64-bit: use arbitrary precision, or feed the digits through the modulo in
chunks.
IBAN length by country
| Country | Code | Length | Country | Code | Length |
|---|---|---|---|---|---|
| Albania | AL | 28 | Italy | IT | 27 |
| Austria | AT | 20 | Latvia | LV | 21 |
| Belgium | BE | 16 | Lithuania | LT | 20 |
| Bulgaria | BG | 22 | Luxembourg | LU | 20 |
| Croatia | HR | 21 | Malta | MT | 31 |
| Cyprus | CY | 28 | Netherlands | NL | 18 |
| Czechia | CZ | 24 | Norway | NO | 15 |
| Denmark | DK | 18 | Poland | PL | 28 |
| Estonia | EE | 20 | Portugal | PT | 25 |
| Finland | FI | 18 | Romania | RO | 24 |
| France | FR | 27 | Slovakia | SK | 24 |
| Germany | DE | 22 | Slovenia | SI | 19 |
| Greece | GR | 27 | Spain | ES | 24 |
| Hungary | HU | 28 | Sweden | SE | 24 |
| Iceland | IS | 26 | Switzerland | CH | 21 |
| Ireland | IE | 22 | Türkiye | TR | 26 |
| United Kingdom | GB | 22 |
Norway at 15 and Malta at 31 are the extremes, and the sixteen-character spread between them is the argument against every fixed-length assumption. The standard permits up to 34, so even Malta is not the ceiling — a country could be added tomorrow that is longer.
Sources: ISO 13616 / IBAN standard and the SWIFT IBAN Registry · Verified: 2026-08-04
Testing SEPA and bank transfer flows
- Length validation per country. Derive the expected length from the country code rather than accepting anything in a range. The tool above rejects a German IBAN of 21 characters and says why, which is the behaviour to copy.
- Checksum validation. Run MOD-97 server-side as well as in the browser, with arbitrary precision arithmetic on both sides.
- BIC handling. Under the SEPA IBAN-only rule the BIC is generally not required for euro transfers. If your form demands one, check whether it actually needs it.
- Direct debit mandates. A mandate references the IBAN, so test what happens when a customer changes bank — the mandate has to be re-established, and a flow that silently keeps the old IBAN fails collection later, quietly.
- Error message quality. “Invalid IBAN” tells a customer nothing. “That IBAN is 21 characters; German IBANs are 22” tells them where to look.
Five mistakes that account for most of it
- Assuming a fixed length. Covered above, and still the most common.
- Using a normal number type for MOD-97. Also covered, and the hardest to diagnose.
- Storing the IBAN with spaces. Display in groups of four; store and compare without
them. Otherwise
DE89 3704…andDE893704…are two different rows. - Rejecting lowercase input. People type lowercase. Normalise to uppercase before validating rather than telling them off.
- Not validating the country code. Two letters that are not an ISO 3166-1 country cannot be a valid IBAN, and checking that first produces a much better error message than a checksum failure.
IBAN and PCI DSS
A useful boundary to be clear about: an IBAN is not in scope for PCI DSS. That standard governs payment card data — primary account numbers, security codes, magnetic stripe contents — and a bank account identifier is none of those. Building your bank transfer flow does not drag it into your cardholder data environment.
It is not unregulated, though. An IBAN attached to a person is personal data under GDPR, with the same obligations as any other customer record: a lawful basis, a retention period, and a breach notification duty if it leaks. That is precisely why synthetic IBANs belong in your test environment — the same reasoning as for the test identity generator, and a different compliance regime from the card side rather than a lighter one.
Related tools and guides
For card rather than bank data, the all-network generator produces full records and the bulk generator does it at volume with reproducible seeds. The tool directory lists everything else, and the FAQ covers what a checksum does and does not prove. The payment form testing checklist covers the wider form.