GB98BARC20309966831036 is a perfectly valid IBAN. It passes the ISO 7064 mod-97 check, it is 22 characters long, and its BBAN matches the registry pattern for the UK. It is also an account number that the UK modulus rules reject, because I built it by wrapping a failing test case from the Vocalink specification in freshly computed check digits.
That one string explains most of the bugs I have seen in bank-detail validation. Teams implement the IBAN checksum, see green, and assume the account is real. The checksum proves the string was typed consistently. It says nothing about whether the sort code exists, whether the account number is plausible at that branch, or whether the name on the payment matches the person who owns it. Those are three more layers, and each one fails differently.
This guide walks through all of them, with the current numbers: the Vocalink modulus checking specification is at version 9.10, valid from 17 October 2026, and the weight table it ships with has 1,170 ranges.
How Does IBAN Validation Work?
An IBAN (ISO 13616) has three parts: a two-letter country code, two check digits, and a country-specific Basic Bank Account Number (BBAN). The check digits use ISO 7064 MOD 97-10:
1. Move the first four characters to the end. 2. Replace each letter with two digits: A=10, B=11, through Z=35. 3. Treat the result as one big integer. It is valid if the integer mod 97 equals 1.
To generate check digits instead, put 00 in their place, run the same conversion, and subtract the remainder from 98.
In Python the whole check is one line once the string is normalised:
def iban_ok(s: str) -> bool:
s = s.replace(" ", "").upper()
r = s[4:] + s[:4]
return int("".join(str(int(c, 36)) for c in r)) % 97 == 1
int(c, 36) handles the letter mapping for free, because base 36 maps A to 10 and Z to 35. Python's arbitrary precision integers mean you do not need the piecewise mod trick that languages with 64-bit integers require. In Rust, Go or JavaScript without BigInt, process the string in chunks of nine digits, carrying the remainder forward.
What mod-97 catches is narrower than most people assume. ISO 7064 says it detects all single-character substitutions and "all or nearly all" single transpositions. That is excellent protection against a customer fat-fingering a digit. It is no protection at all against someone pasting a syntactically perfect IBAN that belongs to nobody, or to the wrong person.
IBAN Length by Country: GB, DE, FR, NL and the Rest
Format checks come before the checksum, and they are cheap. Every country in the SWIFT IBAN Registry has a fixed length and a BBAN pattern. Release 101 of the registry lists 89 countries. The ISO maximum is 34 characters, but no country uses it.
| Country | Length | BBAN pattern | National check inside BBAN |
|---|---|---|---|
| Norway (NO) | 15 | 4!n6!n1!n | Yes, mod 11 |
| Belgium (BE) | 16 | 3!n7!n2!n | Yes, first 10 digits mod 97 |
| Netherlands (NL) | 18 | 4!a10!n | Historic 11-test, no longer reliable |
| Germany (DE) | 22 | 8!n10!n | Yes, bank-specific methods |
| United Kingdom (GB) | 22 | 4!a6!n8!n | Yes, Vocalink modulus check |
| Ireland (IE) | 22 | 4!a6!n8!n | Same layout as GB |
| Spain (ES) | 24 | 4!n4!n1!n1!n10!n | Yes, two weighted mod-11 digits |
| France (FR) | 27 | 5!n5!n11!c2!n | Yes, RIB key |
| Italy (IT) | 27 | 1!a5!n5!n12!c | Yes, CIN letter |
| Russia (RU) | 33 | registry pattern | Longest in the registry |
The pattern notation is SWIFT's: 4!a means exactly four letters, 6!n exactly six digits, 11!c exactly eleven alphanumerics.
The right-hand column matters more than the length. Several countries embed their own check digit inside the BBAN. France's RIB key is 97 − ((89 × bank + 15 × branch + 3 × account) mod 97), after mapping any letters in the account number to digits. Belgium uses the first ten digits mod 97, with a zero result written as 97. Spain has two weighted mod-11 digits, one over bank and branch, one over the account. An IBAN library that ignores these will accept French and Spanish IBANs with valid outer check digits and a broken inner one.
What Is UK Sort Code Modulus Checking?
The UK never had a single national check digit. Each bank chose its own algorithm, and Vocalink (now a Mastercard company) publishes the combined rulebook as Validating Account Numbers: UK Modulus Checking. Version 9.10 has a cover date of 5 September 2026 and comes with two data files:
valacdos.txt: the weight table. One line per sort code range: start sort code, end sort code, method, fourteen weights, and an optional exception number.scsubtab.txt: a 21-line sort code substitution table, used only by exception 5. It has been valid since June 2005.
MOD10 and MOD11 multiply each of the 14 digits (six of sort code, eight of account number, labelled u v w x y z a b c d e f g h) by its weight, sum the products, and require the total to divide evenly. DBLAL, double alternate, sums the individual digits of each product instead, then takes mod 10. Some weights are negative.
I parsed the current file: 925 MOD11 lines, 195 DBLAL, 50 MOD10. When a range appears on two consecutive lines, the account must be run through both checks, in file order, and the exception rules say how to combine the results.
Section 1.2 of the spec contains the sentence every product manager should read. A pass means "a possible account number for the sorting code, but does not necessarily mean that it is an account number being used at that sorting code." Modulus checking is a typo filter with bank-specific rules. It does not tell you whether the account exists.
Account numbers that are not eight digits need normalising first. NatWest 10-digit accounts use the last eight digits. Co-operative Bank and Leeds Building Society 10-digit accounts use the first eight. Santander's nine-digit accounts from the old Alliance & Leicester book replace the last digit of the sort code with the first account digit and then use the last eight. Six- and seven-digit numbers are left-padded with zeros.
Vocalink Modulus Exceptions 1 to 14: The Ones That Bite
Fourteen exceptions override the basic algorithm. They are where homegrown implementations go wrong, because the test vectors that exercise them are easy to skip. From my count of the v9.10 file:
| Exception | Ranges | What it does |
|---|---|---|
| 2 | 237 | Lloyds euro accounts: swap weights based on digits a and g |
| 9 | 235 | Paired with 2: if it fails, retry using sort code 309634 |
| 6 | 66 | Foreign-currency accounts: if a is 4–8 and g = h, cannot be checked |
| 10, 11 | 18 each | Lloyds/TSB: valid if either check passes |
| 7 | 15 | If g = 9, zero the weights u to b |
| 14 | 10 | Coutts: if MOD11 fails and the 8th digit is 0, 1 or 9, shift and retry |
| 3, 5, 12, 13 | 4 each | Skip rules, sort code substitution, either-passes rules |
| 1 | 3 | Add 27 to the DBLAL total |
| 4 | 2 | Remainder must equal check digits gh |
| 8 | 1 | Substitute sort code 090126 |
Exception 6 is the one that changes your data model. GoCardless's documentation uses sort code 200000 with account 55779911 as an example request. Run the arithmetic and it fails both checks. But 200000 carries exception 6, and that account number has a = 5 and g = h = 1. Per the spec, it is not invalid. It cannot be checked.
Missing sort codes behave the same way. If a sort code falls into no range in valacdos.txt, section 2.2.1 says the details "should be presumed valid". So the honest return type of a UK modulus check is not a boolean:
enum ModulusResult {
Valid,
Invalid,
NotCheckable { reason: NotCheckableReason }, // Ex6, sort code not in table
}
If your function returns bool, it is either rejecting legitimate foreign-currency Lloyds accounts or accepting every account number at an unlisted sort code without telling you it did.
Can an IBAN Pass Mod-97 and Still Be Wrong?
Yes, and for UK accounts it is easy to show. A GB IBAN is GBkk BBBB SSSSSS AAAAAAAA: country, check digits, four letters (in practice the first four characters of the bank's BIC), a six-digit sort code and an eight-digit account number. The outer checksum covers all of it, but it cannot know whether the inner sort code and account number satisfy the bank's own rule.
Two IBANs I computed from the spec's test vectors (the BARC bank code is illustrative):
GB76BARC20295963748472passes mod-97 and passes the modulus check (spec test case 3).GB98BARC20309966831036passes mod-97 and fails the modulus check (spec test case 27).
The same applies across Europe. The bank code in a German IBAN is supposed to select one of the Bundesbank's check methods, and libraries such as schwifty in Python implement it. ibantools, the most common JavaScript option, does not by default.
IBAN Validation vs Modulus Check vs Confirmation of Payee
These layers answer different questions. Treating any of them as a substitute for the next is the root cause of most misdirected payments I have had to unpick.
| Layer | What it proves | Cost | Latency |
|---|---|---|---|
| Format and length | The string has the right shape for its country | Free, local | Microseconds |
| IBAN mod-97 | No transcription error in the string | Free, local | Microseconds |
| National check (UK modulus, FR RIB, ES CCC) | A plausible number at that branch | Free, local, data needs updating | Microseconds |
| Sort code directory (EISCD) | Sort code exists, which schemes it reaches (Bacs, Faster Payments, CHAPS) | Licence from Pay.UK/Vocalink | Local lookup |
| Confirmation of Payee (UK) / Verification of Payee (EU) | The name matches the account holder | Scheme participation or a provider | Network round trip |
| Commercial lookup API | Bank name, BIC, collectability | Per-call or subscription | Network round trip |
The spec itself says to check the sort code against EISCD before running the modulus check, because ranges in valacdos.txt include sort codes that are not in use. Pay.UK recommends downloading EISCD weekly and requires at least monthly.
Name checking is now mandatory in both markets. In the UK, the PSR's Specific Direction 17 pushed Confirmation of Payee to around 400 PSPs by 31 October 2024, and Pay.UK reports more than 99% of Faster Payments senders covered. In the EU, the Instant Payments Regulation (EU) 2024/886 made Verification of Payee mandatory for euro-area PSPs from 9 October 2025 and for non-euro member states from 9 July 2027. VoP must be free to the payer, and the EPC rulebook returns four outcomes: Match, Close Match, No Match and Verification Not Possible. I covered the API side of that in the Verification of Payee guide.
GoCardless's POST /bank_details_lookups shows what a commercial layer adds on top. You send an iban, or an account_number with branch_code and country_code, optionally with account_holder_name. It returns bank_name, bic, an available_debit_schemes array and payer_name_verification_result (full, close, cannot_perform_verification or null). An empty available_debit_schemes means you cannot collect from that account, even though it may pass every checksum. bic can be null for a collectable account, so do not make it a required field. If you call the endpoint only for validation, without collecting payments, GoCardless asks you to contact them first.
Stripe, by contrast, surfaces the failure later and more coarsely: account_number_invalid, routing_number_invalid, bank_account_unusable, no_account, account_closed, and account_holder_name_verification_failed for Bacs name checks. None of these is IBAN-specific, which is another reason to validate locally before the request leaves your service.
Which IBAN Validation Library Should You Use in 2026?
| Library | Language | National BBAN checks | Data freshness |
|---|---|---|---|
| schwifty 2026.7.3 | Python | DE, IT, BE, FR, ES, PT, PL, FI, NO and more | Active; added Yemen in July 2026 |
| python-stdnum 2.2 | Python | BE, ES, ME, NO only | Registry v101 |
| ibantools 4.5.4 | JavaScript | None by default; plug in via setCountryBBANValidation | Active |
| iban_validate 5.0.1 | Rust | Structure and mod-97 | Last release Oct 2024 |
| uk-modulus-check 2.0.11 | JavaScript | UK modulus only | Bundles Vocalink v8.50, June 2025 |
That last row is the problem. The npm package bundles a weight table from June 2025. Vocalink has shipped roughly seven versions since, mostly adding sort code ranges, such as 092000–093725 as MOD11 from 15 August 2026. Its README also admits that sort codes missing from the table pass with any account number. I found other ports still on v7.90.
Python teams should start with schwifty for IBANs. For UK modulus checking in any language, use a library that loads valacdos.txt at runtime, or write the 200-odd lines yourself against the spec's 34 test vectors. Ship a stale table and a new bank's customers will come back as "not checkable" at best, and wrongly invalid at worst once their range collides with an older one.
Should You Block Payments That Fail a Modulus Check?
My position: hard-block on format and mod-97 failures, soft-block on modulus failures, never block on "not checkable", and treat the weight table as data rather than code.
Format and checksum failures are always typos. Rejecting them at the input field, with a message pointing at the likely bad character, saves a returned payment and a support ticket.
A modulus failure is almost always a typo, but your table might be out of date and the spec does change. Show the error, ask the user to re-check, and let operations override it with a logged reason. A bad account number that only modulus checking would catch is rare. A stale table rejecting a good account is a far more common way for this check to cause an incident.
"Not checkable" must pass through. Exception 6 accounts are real customer accounts, and blocking them is a production bug that shows up as complaints from Lloyds euro account holders.
I also expect Confirmation of Payee and Verification of Payee to push modulus checking further down the stack over the next few years. When the receiving bank answers "Match" or "No Match" in real time, a local check that can only say "plausible" is useful mainly for saving a network call. That still matters for bulk Bacs files, which can opt out of VoP for non-consumer payers in the EU, and for anything you validate offline. It just should not be the last line of defence.
What This Means for Payment Engineers in the UK and EU
1. Layer the checks in order. Format, length, mod-97, national check, directory lookup, then name verification. Stop at the first hard failure.
2. Model three outcomes. Valid, invalid, not checkable. Store which layer produced the result and which data version it used, so you can explain a rejection six months later.
3. Load valacdos.txt as configuration. Subscribe to Vocalink's update alerts, pull new versions into a config store, and run the spec's 34 test vectors on every update. The 2025–26 cadence was a new version every one to three months.
4. Unwrap GB and IE IBANs. Run the modulus check on the inner sort code and account number. A passing IBAN checksum tells you nothing about the BBAN.
5. Do not assume a library does national checks. Check the source. ibantools needs a plugin, python-stdnum covers four countries, schwifty covers most of the eurozone.
6. Keep EISCD current. It tells you whether a sort code can receive Faster Payments or only Bacs, which matters more for routing than whether the account number is plausible. My Faster Payments vs Bacs vs CHAPS guide covers what that means for settlement.
7. Put name verification in front of first payments. CoP and VoP are the only layers that catch a correct account belonging to the wrong person.
None of this is glamorous, but bank-detail validation sits on the path to every payout, refund and direct debit mandate you will ever build. Get the layers right once and the returns queue gets a lot quieter.
Tom Wang writes about payments infrastructure and the engineering behind moving money.