IBAN Validation Guide: Structure, mod-97 Algorithm, and Common Pitfalls
IBAN (International Bank Account Number) is the standard account identifier used across SEPA and many non-European countries. On the surface it looks like a simple alphanumeric string, but validating it correctly in payment forms requires understanding its internal structure and the mod-97 check algorithm. A wrong implementation silently accepts invalid IBANs — or rejects valid ones from countries your form did not anticipate. Use the IBAN Checker to test any IBAN while you read.
This guide also pairs well with the Testers Guide, which covers the broader set of validation tools available in DevToys Pro.
IBAN Structure
Every IBAN is composed of three parts, concatenated without separators (spaces are only for human readability and must be stripped before processing):
| Part | Length | Description |
|---|---|---|
| Country code | 2 letters (A–Z) | ISO 3166-1 alpha-2 country code, e.g. DE, FR, GB |
| Check digits | 2 digits (00–99) | Computed via mod-97; protects against transcription errors |
| BBAN | 11–30 alphanumeric | Basic Bank Account Number — country-specific format encoding sort code, account number, branch, etc. |
Here is a concrete breakdown of a German IBAN:
DE89 3704 0044 0532 0130 00
^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
| | BBAN (18 digits for Germany)
| Check digits: 89
Country code: DE
Stripped: DE89370400440532013000The total length of an IBAN is fixed per country — Germany always uses 22 characters, France always uses 27, and so on. This fixed length is itself a first-pass validation signal: if the stripped IBAN does not match the expected length for its country code, it is invalid without needing to run mod-97.
Country-Specific IBAN Lengths
The BBAN format and total IBAN length vary by country. Below are the most common ones developers encounter in European payment flows:
| Country | Code | Length | BBAN format |
|---|---|---|---|
| Germany | DE | 22 | 8-digit bank/branch code + 10-digit account |
| United Kingdom | GB | 22 | 4-letter bank code + 6-digit sort code + 8-digit account |
| Netherlands | NL | 18 | 4-letter bank code + 10-digit account |
| Spain | ES | 24 | 4-digit bank + 4-digit branch + 2 check + 10-digit account |
| France | FR | 27 | 5-digit bank + 5-digit branch + 11-char account + 2 check |
| Italy | IT | 27 | 1-letter CIN + 5-digit bank + 5-digit branch + 12-char account |
| Poland | PL | 28 | 8-digit bank/branch + 16-digit account |
| Belgium | BE | 16 | 3-digit bank + 7-digit account + 2 check |
| Switzerland | CH | 21 | 5-digit bank + 12-char account |
| United States | US | — | No IBAN — uses ABA routing + account number |
The US, Canada, Australia, and a few others do not participate in the IBAN system. If your form must accept non-IBAN countries, you need a separate validation path for those routing number + account number combinations.
The mod-97 Algorithm
The two check digits embedded in an IBAN are computed using ISO 7064 mod-97-10. The algorithm works as follows:
- Move the first four characters to the end. Take the country code and check digits (positions 0–3) and append them after the BBAN.
DE89370400440532013000 => 370400440532013000DE89 - Convert letters to digits. Replace each letter with its numeric equivalent: A=10, B=11, C=12, ... Z=35. Digits stay as-is.
370400440532013000DE89 => 370400440532013000131489 - Compute the integer modulo 97. Treat the entire string as one large integer and compute
mod 97. The result must equal 1 for a valid IBAN.370400440532013000131489 mod 97 = 1 ✓
If the result is anything other than 1, the IBAN is invalid. Note that check digits of 00, 01, and 99 are never assigned — valid check digits range from 02 to 98.
Implementation: Chunked mod-97
The integer produced by step 2 can have up to 34 digits — too large for a standard 64-bit integer (max ~18 digits). You have two choices: use BigInt, or process the number in chunks. The chunked approach works in any language without big-integer support:
Process the digit string left to right, maintaining a running remainder. For each group of digits, prepend the current remainder and compute mod 97:
function modulo97(numericString) {
let remainder = 0;
for (let i = 0; i < numericString.length; i += 7) {
const chunk = remainder.toString() + numericString.slice(i, i + 7);
remainder = parseInt(chunk, 10) % 97;
}
return remainder;
}Using chunks of 7 digits keeps the intermediate value well within a 32-bit integer (max 9,999,999 prefix + 7-digit chunk = at most 16 digits, which fits in a 53-bit float safely).
Code Examples
JavaScript — BigInt approach
function validateIban(raw) {
// Normalize: strip spaces, uppercase
const iban = raw.replace(/\s+/g, '').toUpperCase();
// Basic format check
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(iban)) return false;
// Country length registry (partial — add more as needed)
const lengths = {
DE: 22, GB: 22, FR: 27, NL: 18, ES: 24,
IT: 27, PL: 28, BE: 16, CH: 21, AT: 20,
};
if (lengths[iban.slice(0, 2)] && iban.length !== lengths[iban.slice(0, 2)]) {
return false;
}
// Rearrange: move first 4 chars to end
const rearranged = iban.slice(4) + iban.slice(0, 4);
// Convert letters to digits (A=10 ... Z=35)
const numeric = rearranged
.split('')
.map(ch => (ch >= 'A' ? (ch.charCodeAt(0) - 55).toString() : ch))
.join('');
// mod-97 using BigInt
return BigInt(numeric) % 97n === 1n;
}JavaScript — chunked approach (no BigInt)
function validateIbanChunked(raw) {
const iban = raw.replace(/\s+/g, '').toUpperCase();
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(iban)) return false;
const rearranged = iban.slice(4) + iban.slice(0, 4);
const numeric = rearranged
.split('')
.map(ch => (ch >= 'A' ? (ch.charCodeAt(0) - 55).toString() : ch))
.join('');
let remainder = 0;
for (let i = 0; i < numeric.length; i += 7) {
const chunk = remainder.toString() + numeric.slice(i, i + 7);
remainder = parseInt(chunk, 10) % 97;
}
return remainder === 1;
}Python
def validate_iban(raw: str) -> bool:
iban = raw.replace(' ', '').upper()
if not iban[:2].isalpha() or not iban[2:4].isdigit():
return False
# Rearrange: move first 4 chars to the end
rearranged = iban[4:] + iban[:4]
# Convert letters to digits (A=10 ... Z=35)
numeric = ''.join(
str(ord(ch) - 55) if ch.isalpha() else ch
for ch in rearranged
)
# Python's int() handles arbitrarily large integers natively
return int(numeric) % 97 == 1Using libraries
For production code, prefer a well-tested library over a hand-rolled implementation. Both handle the full country registry and edge cases:
# JavaScript / Node.js
npm install iban
# Python
pip install python-stdnumimport IBAN from 'iban';
IBAN.isValid('DE89 3704 0044 0532 0130 00'); // true
IBAN.electronicFormat('DE89 3704 0044 0532 0130 00'); // 'DE89370400440532013000'
IBAN.printFormat('DE89370400440532013000'); // 'DE89 3704 0044 0532 0130 00'from stdnum import iban
iban.validate('DE89370400440532013000') # True
iban.format('DE89370400440532013000') # 'DE89 3704 0044 0532 0130 00'What Validation Does NOT Tell You
Passing mod-97 proves that the IBAN is mathematically well-formed — it does not prove that the account exists or is active. A valid IBAN can still fail at payment time if:
- The account has been closed.
- The sort code or bank code within the BBAN is not assigned to any real bank.
- The account number within the BBAN does not exist at that bank.
- The currency or payment scheme is not supported for that country/bank combination.
Live account verification requires a bank API (such as a payment provider's account validation endpoint or an open banking API). Some providers offer pre-submission IBAN verification that checks bank reachability without initiating a transaction — worth the cost for high-value or first-payment flows.
Input UX Best Practices
Payment forms that handle IBANs poorly produce abandonment and support tickets. These patterns reduce friction without sacrificing validation quality:
- Accept spaces and lowercase on input. Users copy IBANs from bank statements that include spaces. Strip whitespace and uppercase before validation — never show an error for formatting the user didn't control.
- Display in groups of 4. After the user finishes typing, reformat the stored value for display as
DE89 3704 0044 0532 0130 00. This is the standard paper format and aids visual verification. - Validate on blur, not on every keystroke. The IBAN is not fully formed until the user finishes typing. Showing errors mid-input is distracting and incorrect.
- Show the expected length once the country is known. If the first two characters are a valid country code, display a hint like "DE IBANs are 22 characters." A progress indicator (characters entered / expected) also helps.
- Offer a "format my IBAN" helper. A single button that strips spaces, uppercases, and reformats the value saves users from manual cleanup.
- Store the electronic format (no spaces) in your database. Display the print format to users, but strip spaces before storing and transmitting.
BIC / SWIFT vs IBAN
IBAN identifies a specific bank account. BIC (Business Identifier Code), also known as SWIFT code, identifies the bank itself. Both serve different roles in payment routing:
| Identifier | Identifies | Format | Required for |
|---|---|---|---|
| IBAN | Individual bank account | Up to 34 alphanumeric chars | SEPA credit transfers, SEPA direct debit |
| BIC / SWIFT | Bank or financial institution | 8 or 11 alphanumeric chars | International (non-SEPA) wire transfers, legacy SWIFT payments |
Within SEPA (Single Euro Payments Area), BIC is optional since 2016 for SEPA credit transfers and since 2014 for direct debits — the IBAN alone is sufficient. For non-SEPA international wires (e.g. USD transfers to a European bank), the BIC is still required alongside the IBAN.
BIC format: AAAABBCCDDD — 4-letter bank code + 2-letter country code + 2-letter location code + optional 3-letter branch code. An 8-character BIC (no branch) is treated as if the branch is XXX.
Common Pitfalls
- Check digits catch only ~1 in 97 random errors. The mod-97 check is designed to catch single-digit transcription errors and transpositions, not arbitrary random input. A completely fabricated IBAN has roughly a 1-in-97 chance of accidentally passing the checksum. Do not treat mod-97 as a security mechanism.
- Country code length mismatch is a common real-world error. A user who works with multiple countries may accidentally transpose digits between a 22-character DE IBAN and a 27-character FR IBAN. Length validation catches this before mod-97 runs.
- GDPR and data minimization. A full IBAN is considered financial personal data under GDPR. Store only what you need for payment processing. If you only need to show the user "which account is on file," mask everything except the last four characters:
**** **** **** **** 3000. - Never log full IBANs. Application logs, error tracking systems (Sentry, Datadog), and analytics pipelines often inadvertently capture form values. Mask or redact IBANs before they reach any logging layer.
- Virtual IBANs. Payment service providers (Stripe, Adyen, Wise) issue virtual IBANs that pass all structural and mod-97 checks but route to the provider's pooled account. These are valid for payment purposes but the underlying bank account belongs to the PSP, not the recipient. Your validation logic should treat them identically — they are structurally correct IBANs.
Validate any IBAN instantly with the IBAN Checker — runs entirely in your browser, no data leaves your machine. For the full range of validation and testing tools, see the Testers Guide.