DevToys Web Pro iconDevToys Web ProBlogu
Imetafsiriwa kwa LocalePack logoLocalePack
Tupatie ukadiriaji:
Jaribu kiendelezi cha kivinjari:
← Back to Blog

IBAN Validation Guide: Structure, mod-97 Algorithm, and Common Pitfalls

9 min read

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):

PartLengthDescription
Country code2 letters (A–Z)ISO 3166-1 alpha-2 country code, e.g. DE, FR, GB
Check digits2 digits (00–99)Computed via mod-97; protects against transcription errors
BBAN11–30 alphanumericBasic 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: DE89370400440532013000

The 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:

CountryCodeLengthBBAN format
GermanyDE228-digit bank/branch code + 10-digit account
United KingdomGB224-letter bank code + 6-digit sort code + 8-digit account
NetherlandsNL184-letter bank code + 10-digit account
SpainES244-digit bank + 4-digit branch + 2 check + 10-digit account
FranceFR275-digit bank + 5-digit branch + 11-char account + 2 check
ItalyIT271-letter CIN + 5-digit bank + 5-digit branch + 12-char account
PolandPL288-digit bank/branch + 16-digit account
BelgiumBE163-digit bank + 7-digit account + 2 check
SwitzerlandCH215-digit bank + 12-char account
United StatesUSNo 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:

  1. 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
  2. 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
  3. 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 == 1

Using 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-stdnum
import 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:

IdentifierIdentifiesFormatRequired for
IBANIndividual bank accountUp to 34 alphanumeric charsSEPA credit transfers, SEPA direct debit
BIC / SWIFTBank or financial institution8 or 11 alphanumeric charsInternational (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.