IBAN Regex Patterns That Actually Work (And Why Simple Regex Isn't Enough)
Country-specific regex patterns for DE, GB, FR, NL plus a generic IBAN regex — and why regex is only step 1 of validation, not the full solution.
IBAN regex patterns are useful as a first-pass filter — they can quickly reject strings that are obviously not IBANs. But regex alone cannot validate an IBAN. The ISO 13616 check digit algorithm (MOD-97) requires integer arithmetic that no regular expression can perform. This guide covers the patterns that actually work, where they fall short, and how to combine them correctly with a check digit calculation.
Why a Simple Regex Is Never Enough
Consider this string: DE89370400440532013001. It is 22 characters, starts with a valid country code, and contains only alphanumeric characters. Every regex you could write would accept it. But it is invalid — the last digit has been changed from 0 to 1, which breaks the MOD-97 check.
The check digits at positions 3–4 of every IBAN encode a mathematical commitment to the rest of the string. Verifying them requires computing int(rearrangedString) % 97 and confirming the result is 1. No regex engine evaluates arithmetic — this is a fundamental limitation of the formalism, not a practical workaround.
Regex is step 1 of IBAN validation, not the full solution. Use it to reject malformed strings early and cheaply. Then compute MOD-97 on the survivors.
Generic IBAN Regex (All Countries)
A generic pattern that matches any structurally plausible IBAN across all 84 supported countries:
// JavaScript
const IBAN_GENERIC = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
# Python
import re
IBAN_GENERIC = re.compile(r'^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$')This pattern requires:
- Exactly 2 uppercase letters (country code)
- Exactly 2 digits (check digits)
- 11 to 30 alphanumeric characters (BBAN — varies by country)
Total length range: 15 (Norway, NO15) to 34 (Saint Lucia, LC32). The generic regex captures this range. It will also accept structurally plausible strings with invalid country codes or wrong lengths — country-specific patterns tighten this.
Always strip whitespace and convert to uppercase before applying any regex:
// JavaScript
function preprocess(raw) {
return raw.replace(/[s-]/g, '').toUpperCase();
}
# Python
def preprocess(raw: str) -> str:
return raw.replace(' ', '').replace('-', '').upper()Country-Specific Regex Patterns
Country-specific patterns constrain the total length and, in some cases, the internal structure. The BBAN format (the national portion after the check digits) varies by country.
Germany (DE) — 22 Characters
German IBANs contain only digits in the BBAN. The Bankleitzahl (8 digits) and account number (10 digits) are all numeric.
// JavaScript
const DE_IBAN = /^DE[0-9]{20}$/;
# Python
DE_IBAN = re.compile(r'^DE[0-9]{20}$')
// Valid: DE89370400440532013000
// Invalid: DE89370400440532013X00 (letter in BBAN)United Kingdom (GB) — 22 Characters
GB IBANs have a 4-letter bank code followed by a 6-digit sort code and 8-digit account number — all defined structure.
// JavaScript
const GB_IBAN = /^GB[0-9]{2}[A-Z]{4}[0-9]{14}$/;
# Python
GB_IBAN = re.compile(r'^GB[0-9]{2}[A-Z]{4}[0-9]{14}$')
// Valid: GB29NWBK60161331926819
// Invalid: GB29NW1K60161331926819 (digit in bank code)France (FR) — 27 Characters
French IBANs embed a 5-digit bank code, 5-digit branch code, 11-character account number, and 2-digit national check digits. The account characters can be alphanumeric.
// JavaScript
const FR_IBAN = /^FR[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$/;
# Python
FR_IBAN = re.compile(r'^FR[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$')
// Valid: FR7630006000011234567890189Netherlands (NL) — 18 Characters
Dutch IBANs have a 4-letter bank code followed by exactly 10 digits.
// JavaScript
const NL_IBAN = /^NL[0-9]{2}[A-Z]{4}[0-9]{10}$/;
# Python
NL_IBAN = re.compile(r'^NL[0-9]{2}[A-Z]{4}[0-9]{10}$')
// Valid: NL91ABNA0417164300Multi-Country Dispatch Pattern
In JavaScript, you can build a lookup table of country-specific patterns and fall back to the generic pattern for countries not explicitly listed:
const COUNTRY_PATTERNS = {
DE: /^DE[0-9]{20}$/,
GB: /^GB[0-9]{2}[A-Z]{4}[0-9]{14}$/,
FR: /^FR[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$/,
NL: /^NL[0-9]{2}[A-Z]{4}[0-9]{10}$/,
AT: /^AT[0-9]{18}$/,
CH: /^CH[0-9]{19}$/,
ES: /^ES[0-9]{22}$/,
IT: /^IT[0-9]{2}[A-Z][0-9]{10}[A-Z0-9]{12}$/,
BE: /^BE[0-9]{14}$/,
PL: /^PL[0-9]{26}$/,
};
const IBAN_GENERIC = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
function matchesStructure(iban) {
const country = iban.slice(0, 2);
const pattern = COUNTRY_PATTERNS[country] ?? IBAN_GENERIC;
return pattern.test(iban);
}Regex as Step 1: The Full Validation Pipeline
Here is the complete two-step pipeline in JavaScript. Regex gates the MOD-97 computation — no point running the arithmetic on obvious garbage.
const IBAN_LENGTHS = {
AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28,
BA: 20, BE: 16, BG: 22, BH: 22, BR: 29,
BY: 28, CH: 21, CR: 22, CY: 28, CZ: 24,
DE: 22, DK: 18, DO: 28, EE: 20, EG: 29,
ES: 24, FI: 18, FO: 18, FR: 27, GB: 22,
GE: 22, GI: 23, GL: 18, GR: 27, GT: 28,
HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23,
IS: 26, IT: 27, JO: 30, KW: 30, KZ: 20,
LB: 28, LC: 32, LI: 21, LT: 20, LU: 20,
LV: 21, LY: 25, MC: 27, MD: 24, ME: 22,
MK: 19, MR: 27, MT: 31, MU: 30, NL: 18,
NO: 15, PK: 24, PL: 28, PS: 29, PT: 25,
QA: 29, RO: 24, RS: 22, SA: 24, SC: 31,
SE: 24, SI: 19, SK: 24, SM: 27, ST: 25,
SV: 28, TL: 23, TN: 24, TR: 26, UA: 29,
VA: 22, VG: 24, XK: 20,
};
function validateIban(raw) {
const iban = raw.replace(/[s-]/g, '').toUpperCase();
// Step 1a: format check
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(iban)) {
return { valid: false, error: 'INVALID_FORMAT' };
}
// Step 1b: country + length check
const country = iban.slice(0, 2);
const expected = IBAN_LENGTHS[country];
if (!expected) return { valid: false, error: 'UNKNOWN_COUNTRY' };
if (iban.length !== expected) return { valid: false, error: 'WRONG_LENGTH' };
// Step 2: MOD-97 check digit (cannot be done with regex)
const rearranged = iban.slice(4) + iban.slice(0, 4);
const numeric = rearranged.replace(/[A-Z]/g, c => String(c.charCodeAt(0) - 55));
// BigInt required — the number is too large for Number
if (BigInt(numeric) % 97n !== 1n) {
return { valid: false, error: 'INVALID_CHECK_DIGITS' };
}
return { valid: true };
}Notice the BigInt requirement for the MOD-97 step. The numeric string for a 32-character IBAN (Saint Lucia) can be 50+ digits — well beyond JavaScript's safe integer range. Always use BigInt in JavaScript, or Python's native arbitrary-precision integers.
What Regex Cannot Do
To be explicit about the limits of pattern matching for IBAN validation:
- Cannot verify check digits. This is the primary limitation — a regex that accepts all valid IBANs also accepts most invalid ones with a single character changed.
- Cannot verify bank existence. An IBAN can pass MOD-97 and reference a bank code that does not exist. Bank data lookup requires a BIC database.
- Cannot verify account existence. No client-side technique can confirm that the account is open and funded — that requires the bank's systems.
For full validation including bank metadata, use the ibanchecker.cash API — it performs all three layers: format, MOD-97, and BIC database lookup — and returns the bank name and BIC alongside the validity result.
Last updated: June 2026
Validate an IBAN instantly
Free IBAN checker — MOD-97 verification, bank lookup, and SEPA status across 84 countries.
Open IBAN Checker →Related Articles