Skip to Content
GuidesValidate IBAN

Validate IBAN

VerifNow validates an International Bank Account Number against the SWIFT registry entry for its country — the exact length and character layout that country uses — and then against the ISO 7064 MOD-97 check digits every IBAN carries. Those are two different questions, and the response answers both separately. It also says whether the account’s country is inside the SEPA area, which is what decides whether you can collect from it by direct debit at all.


Basic IBAN validation

POST https://api.verifnow.io/api/v1/validate/iban X-API-KEY: your_api_key_here Content-Type: application/json { "value": "DE89370400440532013000" }

Example response

{ "valid": true, "message": "Valid IBAN", "normalizedValue": "DE89370400440532013000", "originalValue": "DE89 3704 0044 0532 0130 00", "validationLevel": "STANDARD", "ibanDetails": { "country_code": "DE", "sepa": true, "structure_valid": true, "checksum_valid": true, "length": 22, "expected_length": 22, "formatted": "DE89 3704 0044 0532 0130 00" } }

Spaces and dashes are stripped and letters upper-cased before validation, so you can pass an IBAN exactly as a user typed it from a statement. normalizedValue is the form to store; formatted is the form to show back.

Two answers, not one

ibanDetails reports the two checks separately, because they fail for different reasons and your form should say which.

FieldQuestion it answers
structure_validCould this be an account number in that country? Length and character layout, from the SWIFT registry — 89 countries.
checksum_validWas it typed correctly? The ISO 7064 MOD-97 check digits.
sepaIs the country inside the SEPA schemes’ geographical scope? Which is to say: can a direct debit reach it?

Check digits alone are not enough. FR23111111111111111111111 has correct MOD-97 digits and is 25 characters long, where every French IBAN is 27. A validator that only runs the checksum accepts it — and you find out weeks later, when the bank rejects the mandate. Here it comes back valid: false, checksum_valid: true, structure_valid: false, with the message “A FR IBAN is 27 characters long”.

An unknown country is rejected outright: ZZ77... returns “ZZ is not an IBAN country”.


sepa — can you collect from it?

The SWIFT registry covers 89 countries. The SEPA schemes cover 42 of them. An Egyptian IBAN can be structurally perfect and correctly typed, and no SEPA direct debit mandate can ever collect from it:

{ "valid": true, "message": "Valid IBAN", "ibanDetails": { "country_code": "EG", "sepa": false, "structure_valid": true, "checksum_valid": true } }

valid stays true, because it is a fact about the number and a transfer from Egypt works fine. Branch on sepa when the account will be debited rather than paid.

The list comes from the EPC’s own list of SEPA scheme countries  (EPC409-09 v8.0, 24 December 2025), folded onto the ISO codes accounts actually carry. The cases a hand-written list gets wrong:

In SEPA, and easy to forgetNot in SEPA, and easy to assume
GB — the UK stayed in after BrexitFK — the Falkland Islands are an IBAN country
CH, LI — never in the EUTR, EG, IL, SA
GI (Gibraltar), VA, MC, SM, ADBR, RU, UA
MD, AL, ME, MK — admitted 2025; RS from May 2026

Guernsey, Jersey and the Isle of Man are in SEPA and issue GB IBANs; Réunion and Saint-Pierre-et-Miquelon issue FR ones; the Azores and Madeira PT ones. They need no code of their own.

sepa: true does not mean this bank accepts direct debits. It means the country is in scope, so a payment service provider there may join the schemes. Whether a particular bank has, and is reachable for SDD Core, is published per bank — not per country — and nothing in an IBAN records it. Treat sepa: false as a hard no and sepa: true as “worth attempting”.


What this does and does not tell you

The MOD-97 checksum is designed to catch the mistakes humans actually make: a mistyped digit, two transposed characters, a missing character. It catches essentially all of them.

A valid IBAN is not a bank account that exists. The checksum proves the number is well formed, not that it was ever issued, that the account is open, or that its holder is who your user claims. No API can tell you that from the number alone — confirming an account holder requires a bank network like SEPA name-checking or a penny-drop transfer.

What it does buy you is real: catching a typo at the point of entry, before it becomes a failed transfer, a returned payment fee, and a support ticket days later.

CheckPerformed
Exact length for the country (Germany is always 22, Malta 31)Yes
Character layout for the country (a German BBAN is 18 digits)Yes
Country is in the SWIFT registry at allYes
Country is in the SEPA areaYes
MOD-97 checksum (ISO 7064)Yes
Whitespace, dash and case normalisationYes
Bank identifier resolves to a real institutionNo
Account exists / is openNo
Account holder matches your customer’s nameNo
This bank is reachable for SEPA direct debitsNo

These four are not alike. Two can be answered from the number with a bank registry — which institution the bank code belongs to, and whether that bank has joined the SEPA schemes. Some APIs maintain those registries; VerifNow does not. The other two cannot be answered from the number at all: whether the account exists, and who holds it, are known only to the bank — through Verification of Payee or a penny-drop transfer.


Where to use it

Validate an IBAN at the point of entry — while the user still has their bank details in front of them. An IBAN corrected three days later, by email, costs far more than one corrected in the form.

Typical placements:

  • Payout details in a marketplace seller onboarding
  • Direct debit mandates
  • Supplier bank details in an accounts-payable flow, where a typo means money leaves for nowhere

Example

import { VerifNow } from '@verifnow/sdk'; const client = new VerifNow({ apiKey: process.env.VERIFNOW_API_KEY }); const result = await client.validateIban(form.iban); if (!result.valid) { // The message names the actual problem: wrong length for the country, wrong shape, or a typo // in the check digits. Showing it beats "invalid IBAN". return field.setError(result.message); } const { countryCode, formatted, sepa } = result.ibanDetails; // A payout can go anywhere; a direct debit cannot. Only check this on an account you will debit. if (!sepa) { return field.setError(`Accounts in ${countryCode} cannot be charged by direct debit.`); } await savePayoutDetails({ iban: result.normalizedValue, country: countryCode }); field.setValue(formatted); // show it back the way a statement prints it

Next steps

Last updated on