DEV Community

Cover image for How IBAN Validation Actually Works (and What It Does Not Prove)
Genory Team
Genory Team

Posted on

How IBAN Validation Actually Works (and What It Does Not Prove)

An IBAN validator often looks deceptively simple.

A user enters something like:

DE89 3704 0044 0532 0130 00
Enter fullscreen mode Exit fullscreen mode

The application shows a green checkmark.

Done, right?

Not quite.

A valid-looking IBAN can pass several mathematical and structural checks without proving that the bank account actually exists, belongs to a particular person, or can receive a payment.

For developers, that distinction matters.

Let's look at what IBAN validation really does.

What is inside an IBAN?

An IBAN — International Bank Account Number — is a structured identifier.

At a high level, it contains:

COUNTRY + CHECK DIGITS + BBAN
Enter fullscreen mode Exit fullscreen mode

For example:

DE89 3704 0044 0532 0130 00
Enter fullscreen mode Exit fullscreen mode

can be viewed conceptually as:

DE | 89 | 370400440532013000
Enter fullscreen mode Exit fullscreen mode

The first two characters are the country code.

The next two digits are check digits.

The remaining part is the BBAN — the country-specific Basic Bank Account Number.

The important detail is that the BBAN structure differs by country.

Germany does not use the same internal structure as the Netherlands or the United Kingdom.

So an IBAN validator should do more than simply check:

starts with two letters
Enter fullscreen mode Exit fullscreen mode

Step 1: Normalize the input

Users rarely type an IBAN in exactly one format.

You may receive:

DE89 3704 0044 0532 0130 00
Enter fullscreen mode Exit fullscreen mode

or:

de89370400440532013000
Enter fullscreen mode Exit fullscreen mode

or:

DE89370400440532013000
Enter fullscreen mode Exit fullscreen mode

A validator usually starts by normalizing the value.

Conceptually:

const normalized = iban
  .replace(/\s+/g, "")
  .toUpperCase();
Enter fullscreen mode Exit fullscreen mode

After normalization:

DE89370400440532013000
Enter fullscreen mode Exit fullscreen mode

This normalized value is much easier to validate and store consistently.

A useful rule is:

Store the normalized identifier and add spaces only for display.

Also remember that an IBAN is text.

Do not treat it as a number.

Leading zeros may be meaningful.

Step 2: Check the country code

The first two characters identify the IBAN country or territory format.

For example:

DE → Germany
NL → Netherlands
GB → United Kingdom
Enter fullscreen mode Exit fullscreen mode

A validator should confirm that the prefix represents a supported IBAN format.

If you receive:

ZZ123456789
Enter fullscreen mode Exit fullscreen mode

and ZZ is not part of your supported format table, you should reject it before doing deeper checks.

This also determines which country-specific length and BBAN rules should be applied.

Step 3: Check the expected length

IBANs do not all have the same length.

The required length depends on the country.

That means this is not enough:

if (iban.length >= 15 && iban.length <= 34) {
  // valid?
}
Enter fullscreen mode Exit fullscreen mode

A proper validator should ask:

What is the expected length for this country?
Enter fullscreen mode Exit fullscreen mode

Then compare the normalized input against that exact value.

For example, if a specific country's IBAN format requires 22 characters, then:

21 characters → invalid
22 characters → continue validation
23 characters → invalid
Enter fullscreen mode Exit fullscreen mode

Length validation catches many common input errors immediately:

  • missing characters
  • accidental extra digits
  • truncated copy/paste
  • wrong country format

But length alone obviously does not prove that the IBAN is correct.

Step 4: Validate the BBAN structure

The part after the first four characters is the country-specific BBAN.

This is where country formats become more interesting.

One country may expect:

bank code + account number
Enter fullscreen mode Exit fullscreen mode

Another might include:

bank code + branch code + account number
Enter fullscreen mode Exit fullscreen mode

Some formats contain only digits.

Others allow letters.

For developers, this means a validator may need country-specific patterns.

Conceptually:

const rules = {
  DE: {
    length: 22,
    bbanPattern: /^[0-9]{18}$/
  },
  // additional country definitions...
};
Enter fullscreen mode Exit fullscreen mode

Real implementations need a complete and maintained rule source rather than a tiny hardcoded example like this.

The important principle is:

Validate according to the selected country's structure, not one universal regular expression.

Step 5: The MOD-97 checksum

This is the part most people associate with IBAN validation.

IBANs use a checksum based on modulo 97.

The algorithm is elegant.

Start with a normalized IBAN:

DE89370400440532013000
Enter fullscreen mode Exit fullscreen mode

Move the first four characters to the end:

370400440532013000DE89
Enter fullscreen mode Exit fullscreen mode

Then convert letters to numbers.

The mapping is:

A = 10
B = 11
C = 12
...
Z = 35
Enter fullscreen mode Exit fullscreen mode

So:

D = 13
E = 14
Enter fullscreen mode Exit fullscreen mode

The rearranged value becomes a very large numeric string.

Then calculate:

number mod 97
Enter fullscreen mode Exit fullscreen mode

A valid IBAN produces:

remainder = 1
Enter fullscreen mode Exit fullscreen mode

Conceptually:

if (mod97(convertedValue) === 1) {
  // checksum passes
}
Enter fullscreen mode Exit fullscreen mode

Don't parse the entire value as one JavaScript number

This is an important implementation detail.

The converted numeric string can be much larger than JavaScript's safe integer range.

So this is a bad idea:

const value = Number(hugeIbanNumber);
const remainder = value % 97;
Enter fullscreen mode Exit fullscreen mode

You can lose precision.

Instead, calculate the modulo incrementally.

For example:

function mod97(numberString) {
  let remainder = 0;

  for (const digit of numberString) {
    remainder = (remainder * 10 + Number(digit)) % 97;
  }

  return remainder;
}
Enter fullscreen mode Exit fullscreen mode

This never requires representing the entire giant number at once.

The same concept works in almost any programming language.

What does a passing checksum actually prove?

This is the most important part of the article.

A passing MOD-97 check proves something very specific:

The characters are mathematically consistent with the IBAN checksum.

That is useful.

It can detect many:

  • mistyped digits
  • transcription errors
  • malformed values
  • accidental character changes

But it does not prove that:

  • the account exists
  • the account is open
  • the account belongs to the person entering it
  • the bank details are currently active
  • the account can receive a particular payment
  • the account has a balance
  • the payment will succeed

That difference should also appear in your user interface.

Instead of displaying:

Bank account verified ✓
Enter fullscreen mode Exit fullscreen mode

a format validator should say something more precise:

IBAN format and checksum valid ✓
Enter fullscreen mode Exit fullscreen mode

Those are very different claims.

Format validation vs account verification

A good mental model is to separate validation into layers.

Layer 1: Syntax

Questions such as:

Does it contain allowed characters?
Enter fullscreen mode Exit fullscreen mode

Layer 2: Country and length

Questions such as:

Does the country exist in the supported IBAN format list?
Does this value have the expected number of characters?
Enter fullscreen mode Exit fullscreen mode

Layer 3: BBAN structure

Questions such as:

Does the country-specific bank/account portion match the expected shape?
Enter fullscreen mode Exit fullscreen mode

Layer 4: International checksum

Questions such as:

Does MOD-97 return 1?
Enter fullscreen mode Exit fullscreen mode

Layer 5: National rules

Some countries have additional bank-code or account-number checks.

These rules are not identical across all IBAN countries.

Layer 6: Real-world verification

This is outside normal offline IBAN validation.

Questions such as:

Does this bank account exist?
Who owns it?
Can it receive this payment?
Enter fullscreen mode Exit fullscreen mode

usually require a bank, payment provider, directory or verification service.

Why your API should expose validation details

Returning only this:

{
  "valid": true
}
Enter fullscreen mode Exit fullscreen mode

is often not enough.

A more useful response might look like:

{
  "valid": true,
  "country": "DE",
  "lengthValid": true,
  "structureValid": true,
  "checksumValid": true,
  "nationalCheckPerformed": false
}
Enter fullscreen mode Exit fullscreen mode

Now the calling application understands what was actually checked.

That distinction is especially useful when validation coverage differs between countries.

For example:

{
  "checksumValid": true,
  "nationalCheckPerformed": false
}
Enter fullscreen mode Exit fullscreen mode

should not automatically be interpreted as:

everything about this bank account has been verified
Enter fullscreen mode Exit fullscreen mode

It simply means that one additional validation layer was not performed.

Build useful negative test cases

When testing an IBAN validator, don't just use:

asdf
Enter fullscreen mode Exit fullscreen mode

as your invalid example.

Create targeted failure cases.

Start with a known fixture:

DE89 3704 0044 0532 0130 00
Enter fullscreen mode Exit fullscreen mode

Then modify one property at a time.

Wrong checksum

Change one check digit:

DE88 3704 0044 0532 0130 00
Enter fullscreen mode Exit fullscreen mode

Expected result:

checksum failure
Enter fullscreen mode Exit fullscreen mode

Wrong length

Remove one character:

DE89 3704 0044 0532 0130 0
Enter fullscreen mode Exit fullscreen mode

Expected result:

length failure
Enter fullscreen mode Exit fullscreen mode

Invalid characters

Insert an illegal character:

DE89 3704 0044 0532 0130 @0
Enter fullscreen mode Exit fullscreen mode

Expected result:

character or structure failure
Enter fullscreen mode Exit fullscreen mode

Unsupported country

Use an unsupported prefix:

ZZ89...
Enter fullscreen mode Exit fullscreen mode

Expected result:

unsupported country
Enter fullscreen mode Exit fullscreen mode

Testing one failure at a time makes debugging much easier.

Don't confuse a BIC with IBAN validation

Another common misconception is that a BIC automatically verifies an IBAN.

A BIC identifies a financial institution or branch format.

An IBAN identifies an account-format structure.

Even if both strings look syntactically valid, that does not automatically prove that:

this IBAN belongs to this BIC
Enter fullscreen mode Exit fullscreen mode

That relationship requires additional reference data or provider-level verification.

So treat them as separate validation problems unless you explicitly have data linking them.

Don't use random strings as payment test data

If you are building a real payment integration, you normally should not invent arbitrary bank details and attempt to process them.

Use the sandbox or test credentials supplied by your payment provider.

Synthetic IBANs are useful for things like:

  • form validation
  • formatting
  • imports
  • exports
  • database fields
  • UI tests
  • checksum handling
  • API schemas

But provider-issued sandbox data should be used for actual payment-flow testing.

A simple IBAN validation pipeline

A practical validation pipeline might look like this:

INPUT
  ↓
Normalize spaces and case
  ↓
Check allowed characters
  ↓
Read country code
  ↓
Check country-specific length
  ↓
Check BBAN structure
  ↓
Run MOD-97
  ↓
Run available national checks
  ↓
Return detailed result
Enter fullscreen mode Exit fullscreen mode

Notice what is not in that pipeline:

Ask bank whether account exists
Enter fullscreen mode Exit fullscreen mode

That is a completely different operation.

Try validation yourself

You can inspect these layers with the Genory IBAN Validator.

It reports country format, length, BBAN structure and MOD-97 results separately rather than treating every successful check as proof of an existing account.

For broader validation tools, Genory also has a dedicated validator collection.

And if you're creating repeatable test cases programmatically, the current developer documentation is available at genory.dev/docs.

A useful rule for your UI

If your software performs offline IBAN validation, avoid messages like:

Account verified
Enter fullscreen mode Exit fullscreen mode

Prefer language such as:

IBAN format is valid
Enter fullscreen mode Exit fullscreen mode

or:

Format and checksum passed
Enter fullscreen mode Exit fullscreen mode

That is more accurate for both developers and users.

Final thought

IBAN validation is a great example of why the word valid can be dangerous in software.

A value can be:

syntactically valid
structurally valid
checksum valid
Enter fullscreen mode Exit fullscreen mode

without being:

a verified real-world bank account
Enter fullscreen mode Exit fullscreen mode

Good validation code does more than return true or false.

It makes clear which claim was actually proven.

Top comments (0)