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.
| Field | Question it answers |
|---|---|
structure_valid | Could this be an account number in that country? Length and character layout, from the SWIFT registry — 89 countries. |
checksum_valid | Was it typed correctly? The ISO 7064 MOD-97 check digits. |
sepa | Is 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 forget | Not in SEPA, and easy to assume |
|---|---|
| GB — the UK stayed in after Brexit | FK — the Falkland Islands are an IBAN country |
| CH, LI — never in the EU | TR, EG, IL, SA |
| GI (Gibraltar), VA, MC, SM, AD | BR, 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.
| Check | Performed |
|---|---|
| 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 all | Yes |
| Country is in the SEPA area | Yes |
| MOD-97 checksum (ISO 7064) | Yes |
| Whitespace, dash and case normalisation | Yes |
| Bank identifier resolves to a real institution | No |
| Account exists / is open | No |
| Account holder matches your customer’s name | No |
| This bank is reachable for SEPA direct debits | No |
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 itNext steps
- Validating an EU B2B checkout — reverse charge, direct debit, and what to do when VIES is down mid-payment
- VAT number and IBAN in one call — both fields on one billing form
- Validate VAT — the other field a billing form depends on
- Node.js example — full integration with
@verifnow/sdk - Error Handling — status codes and transport failures