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 mod97 checksum (modulus 97, ISO 13616) — 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 IsValid SDK or the native fetch API.
import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Example usage ──────────────────────────────────────────────────────────── const result = await iv.iban('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); }
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
Node.js integration notes
When handling IBAN in a TypeScript codebase, define a branded type to prevent accidental mixing of financial identifier strings at compile time:type Iban = string & { readonly __brand: 'Iban' }. The IsValid SDK ships with full TypeScript definitions covering all response fields, including country-specific and instrument-specific data, so your editor provides autocomplete on the parsed result without manual type declarations.
In financial data pipelines — payment processors, reconciliation engines, or KYC workflows — IBAN validation sits at the ingestion boundary. Pair the IsValid SDK with decimal.js orbig.js for any monetary amounts tied to the identifier, and usepino for structured logging that attaches the validation result to the transaction reference in every log line, making audit trails straightforward.
Express.js and Fastify middleware
Centralise IBAN validation in a request middleware rather than repeating it in every route handler. The middleware calls the IsValid API, attaches the parsed result toreq.validated, and callsnext() on success. Layer in a Redis cache keyed by the normalised identifier with a 24-hour TTL to avoid redundant API calls for the same value across multiple requests in the same session.
Error handling should distinguish between a 422 response from IsValid (the IBAN is structurally invalid — return this to the caller immediately) and 5xx or network errors (transient failures — retry once after a short delay before surfacing a service-unavailable error). Never swallow validation failures silently; they indicate bad data that could propagate into financial records downstream.
- Assert
process.env.ISVALID_API_KEYis present at server startup, not lazily at first request - Use
Promise.allSettled()for batch validation — it collects all results without aborting on the first failure - Mock the IsValid client with
jest.mock()in unit tests; keep CI pipelines free of real API calls - Store the full parsed API response alongside the raw IBAN in your database — country code, institution data, and check-digit status are useful for downstream logic
When making HTTP calls to the IsValid API directly (without the SDK), the choice between fetch and axios is largely a matter of preference. The native fetch API is available in Node.js 18+ without any additional dependency and is sufficient for simple request/response flows. axios adds automatic JSON parsing, request/response interceptors, and a cleaner timeout API (axios.create({ timeout: 5000 })), which makes it easier to centralise the Authorization header and retry logic in one place. For high-throughput services that make many concurrent API calls, consider undici — the HTTP client underlying Node.js fetch — used directly for its connection pooling and lower overhead.
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.