All entries
Chapter VIII
Journal · 17 Oct 2026 · 13 min read

IBAN Validation and UK Modulus Checks Explained

IBAN mod-97 vs UK sort code modulus checking: the Vocalink v9.10 rules, exceptions 1 to 14, stale library data and where Confirmation of Payee fits.

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.

CountryLengthBBAN patternNational check inside BBAN
Norway (NO)154!n6!n1!nYes, mod 11
Belgium (BE)163!n7!n2!nYes, first 10 digits mod 97
Netherlands (NL)184!a10!nHistoric 11-test, no longer reliable
Germany (DE)228!n10!nYes, bank-specific methods
United Kingdom (GB)224!a6!n8!nYes, Vocalink modulus check
Ireland (IE)224!a6!n8!nSame layout as GB
Spain (ES)244!n4!n1!n1!n10!nYes, two weighted mod-11 digits
France (FR)275!n5!n11!c2!nYes, RIB key
Italy (IT)271!a5!n5!n12!cYes, CIN letter
Russia (RU)33registry patternLongest 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.
There are three methods. 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:

ExceptionRangesWhat it does
2237Lloyds euro accounts: swap weights based on digits a and g
9235Paired with 2: if it fails, retry using sort code 309634
666Foreign-currency accounts: if a is 4–8 and g = h, cannot be checked
10, 1118 eachLloyds/TSB: valid if either check passes
715If g = 9, zero the weights u to b
1410Coutts: if MOD11 fails and the 8th digit is 0, 1 or 9, shift and retry
3, 5, 12, 134 eachSkip rules, sort code substitution, either-passes rules
13Add 27 to the DBLAL total
42Remainder must equal check digits gh
81Substitute 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):

  • GB76BARC20295963748472 passes mod-97 and passes the modulus check (spec test case 3).
  • GB98BARC20309966831036 passes mod-97 and fails the modulus check (spec test case 27).
Generate check digits for any garbage BBAN and the IBAN checksum will happily pass. That is exactly what an attacker, a buggy CSV export or a copy-paste from the wrong row will produce. For GB and IE IBANs, unwrap the BBAN and run the national check too.

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.

LayerWhat it provesCostLatency
Format and lengthThe string has the right shape for its countryFree, localMicroseconds
IBAN mod-97No transcription error in the stringFree, localMicroseconds
National check (UK modulus, FR RIB, ES CCC)A plausible number at that branchFree, local, data needs updatingMicroseconds
Sort code directory (EISCD)Sort code exists, which schemes it reaches (Bacs, Faster Payments, CHAPS)Licence from Pay.UK/VocalinkLocal lookup
Confirmation of Payee (UK) / Verification of Payee (EU)The name matches the account holderScheme participation or a providerNetwork round trip
Commercial lookup APIBank name, BIC, collectabilityPer-call or subscriptionNetwork 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?

LibraryLanguageNational BBAN checksData freshness
schwifty 2026.7.3PythonDE, IT, BE, FR, ES, PT, PL, FI, NO and moreActive; added Yemen in July 2026
python-stdnum 2.2PythonBE, ES, ME, NO onlyRegistry v101
ibantools 4.5.4JavaScriptNone by default; plug in via setCountryBBANValidationActive
iban_validate 5.0.1RustStructure and mod-97Last release Oct 2024
uk-modulus-check 2.0.11JavaScriptUK modulus onlyBundles 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.