Every payments, payroll or invoicing product eventually adds an IBAN field, and then a support ticket arrives: "The transfer bounced, the IBAN was wrong." A good IBAN check at input time catches most of these before they cost you a failed SEPA payment and a confused customer.
Here's what a proper IBAN validation involves, and how to get it from one API call.
Anatomy of an IBAN
Take DE89 3704 0044 0532 0130 00:
| Part | Value | Meaning |
|---|---|---|
| Country code | DE |
ISO 3166 country |
| Check digits | 89 |
mod-97 checksum over the whole IBAN |
| BBAN | 370400440532013000 |
The national account number (Basic Bank Account Number) |
| Bank code | 37040044 |
German Bankleitzahl, here Commerzbank in Cologne |
| Account | 0532013000 |
The account itself |
The BBAN's structure is different in every country. Germany is 8 digits bank code + 10 digits account (22 characters total). France is 27 characters with a 5-digit bank code, 5-digit branch, 11 alphanumeric account characters and a 2-digit national key. Malta is 31 characters. Dozens of countries and territories use IBANs (VatPulse carries the rules for 127 of them), each with its own structure.
The four checks that matter
1. Characters and country. Strip spaces, uppercase, check it starts with a real IBAN country code.
2. Length. Each country has exactly one valid length. DE with 21 characters is wrong, full stop.
3. Structure. The SWIFT IBAN registry defines the BBAN pattern, like 8!n10!n for Germany (8 digits, then 10 digits). Letters where digits should be means it's invalid.
4. mod-97 checksum. Move the first four characters to the end, replace letters with numbers (A=10 ... Z=35), and the remainder modulo 97 must be 1:
function mod97(iban) {
const s = iban.slice(4) + iban.slice(0, 4);
let rem = 0;
for (const ch of s) {
const v = /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch;
for (const d of v) rem = (rem * 10 + Number(d)) % 97;
}
return rem === 1;
}
This catches all single-character typos and almost all transpositions, which are the most common data-entry errors.
What a checksum can't tell you: whether the account actually exists or is open. Only the bank (or a paid account verification service) knows that. Be wary of any API that claims otherwise from the IBAN alone.
The useful extra: bank name and BIC
Users like seeing "Commerzbank" appear under the field; it reassures them they typed the right thing. And many older payment integrations still need a BIC.
Once you know where the bank code sits in the BBAN, you can look it up in national bank directories. Central banks and payment associations publish these, for example the Deutsche Bundesbank's Bankleitzahlen file, the Austrian OeNB directory, Belgium's NBB list and the Dutch Betaalvereniging BIC list.
Keeping a dozen national files up to date is a chore, so I bundled them into the API.
One API call
VatPulse, the VAT & IBAN validation API I built, does all of the above offline, so it's fast and never depends on an external service being up:
curl -X POST https://vatpulse1.p.rapidapi.com/validate \
-H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
-H "X-RapidAPI-Host: vatpulse1.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"iban": "DE89 3704 0044 0532 0130 00"}'
{
"iban": {
"valid": true,
"iban": "DE89370400440532013000",
"iban_formatted": "DE89 3704 0044 0532 0130 00",
"country_code": "DE",
"check_digits": "89",
"bban": "370400440532013000",
"bank_code": "37040044",
"account_number": "0532013000",
"sepa": true,
"bank": { "name": "Commerzbank Köln", "bic": "COBADEFFXXX" },
"source": "offline",
"cached": false,
"error": null
},
"billable": true
}
A typo produces a precise error instead of a generic "invalid":
{ "iban": { "valid": false, "error": { "code": "invalid_checksum", "message": "The IBAN check digits are wrong (mod-97 checksum failed). Likely a typo." } } }
Possible codes: invalid_format, invalid_country, invalid_length, invalid_checksum. That lets you show helpful messages ("German IBANs are 22 characters, you entered 21").
Bank/BIC coverage
Checksum, length and structure checks cover 127 IBAN countries and territories. Bank name/BIC lookup covers 43 countries (about 21,900 bank codes) from openly published national registries, including Germany, Austria, Belgium, the Netherlands, Spain, Italy, Switzerland, Poland, Nordic and Baltic countries and others; for France, the UK, Ireland and Portugal it covers the major banks. When a bank code isn't in the dataset, bank is simply null; the IBAN is still fully validated. The docs list exact coverage per country.
Validate VAT and IBAN together
Onboarding a B2B customer? Send both in one request; it counts as one validation:
{ "vat": "IE6388047V", "iban": "IE29AIBK93115212345678" }
Cost model
Only fresh checks count toward the quota, whether the IBAN is valid or invalid. Repeat lookups of the same IBAN within 24 hours are cache hits and free, and malformed input rejected before the checksum is free too. (The IBAN itself is never stored, only a keyed hash used as a 24-hour cache marker.)
Try it
- Free tier (100 validations/month) on RapidAPI: https://rapidapi.com/vatpulse/api/vatpulse1
- Docs: https://api.vatpulse.workers.dev/docs
Disclosure: I built VatPulse, the API used in the examples above. The patterns work just as well if you build them yourself.
Top comments (0)