IBAN Validation in Node.js — mod-97 Algorithm Explained
IBANs look simple but have 76 country-specific formats and a two-step checksum that catches most transcription errors. Here's how it all fits together.
In this guide
1. What is an IBAN?
An IBAN (International Bank Account Number) is a standardised way to identify a bank account internationally, defined by ISO 13616. Introduced in 1997 to streamline cross-border payments in Europe, IBANs are now used in over 80 countries — and are mandatory for SEPA credit transfers and direct debits in the Eurozone.
Despite the name, IBANs are not just European. Countries like Brazil, Saudi Arabia, Israel, and Pakistan have adopted the standard. When your payment form accepts IBANs from international users, you need a validator that knows the rules for every country.
2. IBAN anatomy — country code, check digits, BBAN
Every IBAN has the same four-component structure:
Country code (2 letters)
ISO 3166-1 alpha-2 country code. Tells you which country's banking system the account belongs to and determines the expected total length and BBAN format.
Check digits (2 digits)
Two numeric digits calculated using the mod-97 algorithm. They allow detection of single-character transpositions and most common transcription errors — the same principle used in LEI codes.
BBAN — Basic Bank Account Number
The domestic account number, whose format is country-specific. A German BBAN encodes the bank code (Bankleitzahl) and account number. A UK BBAN encodes the sort code and account number. The BBAN length and character set vary by country.
| Country | Total length | Example |
|---|---|---|
| Norway | 15 chars | NO93 8601 1117 947 |
| Germany | 22 chars | DE89 3704 0044 0532 0130 00 |
| United Kingdom | 22 chars | GB29 NWBK 6016 1331 9268 19 |
| France | 27 chars | FR76 3000 6000 0112 3456 7890 189 |
| Poland | 28 chars | PL61 1090 1014 0000 0712 1981 2874 |
| Malta | 31 chars | MT84 MALT 0110 0001 2345 MTLC AST0 01S |
3. The mod-97 checksum — step by step
The check digits are validated by rearranging the IBAN and computing the remainder when divided by 97. A valid IBAN always gives a remainder of 1. Let's walk through it with GB82 WEST 1234 5698 7654 32:
Step 1 — Move the first 4 characters to the end
WEST12345698765432GB82
Step 2 — Replace each letter with its numeric value (A=10, B=11, … Z=35)
3214282912345698765432161182
W=32, E=14, S=28, T=29 · G=16, B=11
Step 3 — Compute the remainder modulo 97
Process in 9-digit chunks (the number is too large for a standard integer). The remainder must equal 1 for the IBAN to be valid.
3214282912345698765432161182 mod 97 = 1 ✓
Here's the algorithm implemented in JavaScript:
// mod97.js — implements the ISO 13616 IBAN checksum function mod97(iban) { // Step 1: move first 4 chars to the end const rearranged = iban.slice(4) + iban.slice(0, 4); // Step 2: replace each letter with its numeric value const numeric = rearranged.replace(/[A-Z]/g, ch => String(ch.charCodeAt(0) - 55) // A=10, B=11, ..., Z=35 ); // Step 3: mod 97 in chunks (JavaScript can't handle the full integer) let remainder = 0; for (let i = 0; i < numeric.length; i += 9) { const chunk = String(remainder) + numeric.slice(i, i + 9); remainder = parseInt(chunk, 10) % 97; } return remainder; // must be 1 for a valid IBAN } const iban = 'GB82WEST12345698765432'; // no spaces console.log(mod97(iban)); // → 1 ✓
4. Why IBAN validation is harder than it looks
76 different lengths
Every country has a fixed total length for its IBANs, ranging from 15 (Norway) to 34 (Saint Lucia). A German IBAN is always 22 characters; a Polish IBAN is always 28. Passing mod-97 with the wrong length still means the IBAN is invalid.
Country-specific BBAN character sets
Some countries allow alphanumeric BBANs (e.g. UK sort codes include letters like WEST), while others are purely numeric (Germany, France). A BBAN with letters for a purely-numeric country is invalid even if the mod-97 check passes.
Formatting is not part of the standard
IBANs are printed in groups of four characters separated by spaces, but the spaces are for human readability only and must be stripped before validation. Users may also enter dashes or no separators at all.
// All of these represent the same IBAN — normalise first 'GB82 WEST 1234 5698 7654 32' // paper/display format 'GB82-WEST-1234-5698-7654-32' // dashes 'GB82WEST12345698765432' // no separators (canonical)
New countries keep joining
The list of IBAN-participating countries grows over time. Hardcoding a country table in your app risks silently rejecting valid IBANs from newly-added countries. Maintaining that table requires ongoing attention.
5. The production-ready solution
The IsValid IBAN API handles format normalisation, country-length validation, and the mod-97 checksum in a single GET request — for 76 countries. The response also includes the parsed BBAN and an isEU flag for SEPA routing logic.
Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.
Full parameter reference and response schema: IBAN Validation API docs →
6. Node.js code example
Using the native fetch API (Node 18+). No dependencies required.
// ibanValidator.js const API_KEY = process.env.ISVALID_API_KEY; const BASE_URL = 'https://api.isvalid.dev'; /** * Validate an IBAN using the IsValid API. * * @param {string} iban - IBAN in any format (spaces, dashes, or none) * @returns {Promise<object>} Validation result */ async function validateIban(iban) { const params = new URLSearchParams({ value: iban }); const response = await fetch(`${BASE_URL}/v0/iban?${params}`, { headers: { Authorization: `Bearer ${API_KEY}` }, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(`IBAN API error ${response.status}: ${error.message ?? 'unknown'}`); } return response.json(); } // ── Example usage ──────────────────────────────────────────────────────────── const result = await validateIban('GB82 WEST 1234 5698 7654 32'); if (!result.valid) { console.log('Invalid IBAN'); } else { console.log(`Valid IBAN — ${result.countryName} (${result.isEU ? 'SEPA' : 'non-SEPA'})`); console.log('BBAN:', result.bban); console.log('Formatted:', result.formatted); if (result.bankName) console.log('Bank:', result.bankName); if (result.bankBic) console.log('BIC:', result.bankBic); // → Valid IBAN — United Kingdom (non-SEPA) // → BBAN: WEST12345698765432 // → Formatted: GB82 WEST 1234 5698 7654 32 // → Bank: (name from registry, if available) }
In a payment form handler, you might use it like this:
// routes/payment.js (Express) app.post('/payment', async (req, res) => { const { iban, amount, ...rest } = req.body; let ibanCheck; try { ibanCheck = await validateIban(iban); } catch { return res.status(502).json({ error: 'IBAN validation service unavailable' }); } if (!ibanCheck.valid) { return res.status(400).json({ error: 'Invalid IBAN' }); } // Use isEU to route SEPA vs international wire const transferType = ibanCheck.isEU ? 'sepa' : 'international'; await initiateTransfer({ iban, transferType, amount, ...rest }); res.json({ success: true, transferType }); });
isEU flag to automatically choose between a SEPA transfer (cheaper, 1 business day) and an international wire for non-Eurozone IBANs. The formatted field gives you the print-friendly grouped format to display back to the user.7. cURL example
Validate a German IBAN:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/iban?value=DE89370400440532013000"
Spaces are handled automatically:
curl -G -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "value=GB82 WEST 1234 5698 7654 32" \ "https://api.isvalid.dev/v0/iban"
With explicit country code (when the IBAN has no prefix):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/iban?value=370400440532013000&countryCode=DE"
8. Understanding the response
Valid German IBAN:
{ "valid": true, "countryCode": "DE", "countryName": "Germany", "bban": "370400440532013000", "isEU": true, "formatted": "DE89 3704 0044 0532 0130 00", "bankCode": "37040044", "bankName": "Commerzbank", "bankBic": "COBADEFFXXX" }
Invalid IBAN (bad checksum):
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Format, length, and mod-97 checksum all pass |
| countryCode | string | 2-letter ISO 3166-1 country code from the IBAN prefix |
| countryName | string | Full country name |
| bban | string | The domestic account number portion (after the 4-char IBAN header) |
| isEU | boolean | Whether the country is an EU member state (SEPA eligible) |
| formatted | string | Human-readable IBAN grouped in blocks of 4 characters |
| bankCode | string | null | National bank code extracted from the BBAN (e.g. 8-digit Bankleitzahl for DE, 8-digit numer rozliczeniowy for PL) |
| bankName | string | null | Name of the bank identified by the bank code, from official central bank registries |
| bankBic | string | null | BIC/SWIFT code of the bank if available from the national registry |
9. Edge cases to handle
SEPA vs. non-SEPA routing
Not all IBAN countries participate in SEPA. Norway, Iceland, Liechtenstein, and Switzerland have IBANs but are not EU members — though they are part of SEPA through EEA/bilateral agreements. Use the isEU flag as a starting point, but apply your own SEPA country list for precise routing.
Accounts without IBANs
The US, Canada, Australia, China, and many other countries do not use IBANs. When accepting international payments, always provide a fallback for account number + routing number / sort code / BSB input for non-IBAN countries.
function showCorrectAccountForm(country) { if (ibanCountries.has(country)) { showIbanInput(); } else { showRoutingAndAccountInput(country); // ABA, sort code, BSB, etc. } }
Input normalisation
Users paste IBANs from documents or emails in many formats. The API strips spaces and hyphens and uppercases the input automatically. Pass the raw user input — no need to pre-process it. The formatted field in the response gives you a clean display version to show back to the user.
Storing IBANs
Store the normalised form (no spaces, uppercase) in your database — e.g. GB82WEST12345698765432. Generate the display format on the fly from the formatted field when needed. This avoids inconsistencies from different input formats.
Summary
See also
Validate IBANs instantly
Free tier includes 100 API calls per day. No credit card required. Supports 76 countries including all EU member states and major non-EU IBAN countries.