npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iban-validate

v1.0.0

Published

Comprehensive IBAN validator. Supports 116 country codes — all official ISO 13616 countries plus experimental African countries. Zero dependencies.

Downloads

119

Readme

iban-validation

npm version npm downloads CI License: MIT TypeScript Zero dependencies

A comprehensive, zero-dependency IBAN validator for TypeScript and JavaScript.

Built on the ISO 7064 MOD-97-10 algorithm and the official SWIFT IBAN registry, covering every country in the world that uses IBAN for banking transactions — from Germany and the UK to Saudi Arabia, Ivory Coast, and Brazil.

import { isValid } from "iban-validate";

isValid("DE89 3704 0044 0532 0130 00"); // true  ✓
isValid("GB29 NWBK 6016 1331 9268 19"); // true  ✓
isValid("DE99 3704 0044 0532 0130 00"); // false ✗ bad checksum

Features

  • 116 country codes — 94 official ISO 13616 countries + 22 experimental (full list below)
  • Typed error union — know exactly why an IBAN failed, not just that it did
  • Discriminated union result — TypeScript narrows the type automatically on isValid
  • Rich country metadata — name, IBAN length, SEPA membership, and a sample IBAN per country
  • Input-tolerant — strips spaces and handles lowercase automatically
  • Dual CJS + ESM build — works in Node.js, Deno, Bun, React, Next.js, and browsers
  • Tree-shakeable — import only what you need
  • Zero dependencies — nothing in dependencies, only dev tooling
  • Every registry example tested — the test suite validates the sample IBAN for all 116 countries

Table of contents


Installation

# npm
npm install iban-validate

# yarn
yarn add iban-validate

# pnpm
pnpm add iban-validate

# bun
bun add iban-validate

Quick start

import {
  isValid,
  validate,
  getCountryInfo,
  getSupportedCountries,
  getSepaCountries,
  getExperimentalCountries,
  getExpectedLength,
} from "iban-validate";

// ── Simple boolean check ───────────────────────────────────────────────────
isValid("DE89 3704 0044 0532 0130 00"); // true  (spaces stripped automatically)
isValid("gb29 nwbk 6016 1331 9268 19"); // true  (lowercase accepted)
isValid("BADINPUT"); // false

// ── Country-constrained check ──────────────────────────────────────────────
isValid("DE89370400440532013000", { countryCca2: "DE" }); // true
isValid("DE89370400440532013000", { countryCca2: "FR" }); // false

// ── Full validation result with typed error ────────────────────────────────
const result = validate("DE99370400440532013000");
if (!result.isValid) {
  console.log(result.error); // 'checksumFailed'
  console.log(result.errorMessage); // 'The mod-97 checksum failed...'
  console.log(result.countryInfo?.countryName); // 'Germany'
}

// ── TypeScript discriminated union — no type assertions needed ─────────────
const r = validate("GB29NWBK60161331926819");
if (r.isValid) {
  r.countryInfo.countryName; // 'United Kingdom' — TypeScript knows this is defined
  r.error; // undefined — TypeScript knows this too
}

// ── Country metadata ───────────────────────────────────────────────────────
const info = getCountryInfo("SA")!;
info.countryName; // 'Saudi Arabia'
info.ibanLength; // 24
info.isSepa; // false
info.isExperimental; // false
info.example; // 'SA4420000001234567891234'

// ── Enumerate countries ────────────────────────────────────────────────────
const all = getSupportedCountries(); // ['AD', 'AE', ...] — 116 codes
const sepa = getSepaCountries(); // SEPA zone only
const exp = getExperimentalCountries(); // Africa + Iran experimental
const len = getExpectedLength("NO"); // 15 (shortest IBAN in the world)

Framework usage

React

Simple input validation

import { validate } from "iban-validate";
import { useState } from "react";

function IbanInput() {
  const [iban, setIban] = useState("");
  const [error, setError] = useState<string | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setIban(value);

    // Only validate once the user has typed enough characters
    if (value.replace(/\s/g, "").length >= 15) {
      const result = validate(value);
      setError(result.isValid ? null : (result.errorMessage ?? null));
    } else {
      setError(null);
    }
  };

  return (
    <div>
      <input
        value={iban}
        onChange={handleChange}
        placeholder="DE89 3704 0044 0532 0130 00"
        style={{ borderColor: error ? "red" : undefined }}
      />
      {error && <p style={{ color: "red", fontSize: 13 }}>{error}</p>}
      {!error && iban && (
        <p style={{ color: "green", fontSize: 13 }}>✓ Valid IBAN</p>
      )}
    </div>
  );
}

With React Hook Form

import { useForm } from "react-hook-form";
import { isValid } from "iban-validate";

type FormValues = { iban: string };

function PaymentForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>();

  const onSubmit = (data: FormValues) => {
    console.log("Validated IBAN:", data.iban.replace(/\s/g, "").toUpperCase());
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register("iban", {
          required: "Please enter an IBAN.",
          validate: (value) => isValid(value) || "Please enter a valid IBAN.",
        })}
        placeholder="GB29 NWBK 6016 1331 9268 19"
      />
      {errors.iban && <p style={{ color: "red" }}>{errors.iban.message}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

With Zod

import { z } from "zod";
import { isValid } from "iban-validate";

const paymentSchema = z.object({
  recipientName: z.string().min(2),
  iban: z.string().refine(isValid, {
    message: "Please enter a valid IBAN.",
  }),
  amount: z.number().positive(),
  currency: z.string().length(3),
});

type Payment = z.infer<typeof paymentSchema>;

Country-locked field (when you know the user's country)

import { validate, getCountryInfo } from "iban-validate";

function CountryLockedIbanField({ countryCode }: { countryCode: string }) {
  const info = getCountryInfo(countryCode);
  const [error, setError] = useState<string | null>(null);

  const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
    const result = validate(e.target.value, { countryCca2: countryCode });
    setError(result.isValid ? null : (result.errorMessage ?? null));
  };

  return (
    <div>
      <label>
        Bank account (IBAN)
        {info && (
          <span style={{ color: "#888", fontSize: 12 }}>
            {" "}
            — {info.countryName} · {info.ibanLength} characters
          </span>
        )}
      </label>
      <input
        onBlur={handleBlur}
        placeholder={info?.example}
        maxLength={info?.ibanLength ? info.ibanLength + 6 : undefined}
      />
      {error && <p style={{ color: "red", fontSize: 13 }}>{error}</p>}
    </div>
  );
}

Displaying country info after successful validation

import { validate } from "iban-validate";

function IbanResult({ iban }: { iban: string }) {
  const result = validate(iban);

  if (!result.isValid) {
    return <p style={{ color: "red" }}>✗ {result.errorMessage}</p>;
  }

  const { countryInfo, cleanedIban } = result;

  return (
    <div style={{ padding: 12, border: "1px solid green", borderRadius: 8 }}>
      <p>✓ Valid IBAN</p>
      <p>
        <strong>Country:</strong> {countryInfo.countryName}
      </p>
      <p>
        <strong>SEPA zone:</strong> {countryInfo.isSepa ? "Yes" : "No"}
      </p>
      <p>
        <strong>Cleaned:</strong> <code>{cleanedIban}</code>
      </p>
    </div>
  );
}

Next.js

API route validation (App Router)

// app/api/payments/route.ts
import { NextRequest, NextResponse } from "next/server";
import { validate } from "iban-validate";

export async function POST(req: NextRequest) {
  const body = await req.json();
  const { iban, countryCode } = body;

  const result = validate(iban, { countryCca2: countryCode });

  if (!result.isValid) {
    return NextResponse.json(
      { error: result.error, message: result.errorMessage },
      { status: 422 },
    );
  }

  return NextResponse.json({
    valid: true,
    iban: result.cleanedIban,
    country: result.countryInfo.countryName,
    isSepa: result.countryInfo.isSepa,
  });
}

Server action (Next.js 14+)

// app/actions/validateIban.ts
"use server";

import { validate } from "iban-validate";

export async function validateIbanAction(formData: FormData) {
  const iban = formData.get("iban") as string;
  const result = validate(iban);

  if (!result.isValid) {
    return { success: false, error: result.errorMessage };
  }

  return {
    success: true,
    cleanedIban: result.cleanedIban,
    countryName: result.countryInfo.countryName,
  };
}

Node.js / Express

import express from "express";
import { validate } from "iban-validate";

const app = express();
app.use(express.json());

app.post("/api/validate-iban", (req, res) => {
  const { iban, country } = req.body;

  if (!iban) {
    return res.status(400).json({ error: "iban is required" });
  }

  const result = validate(iban, { countryCca2: country });

  if (!result.isValid) {
    return res.status(422).json({
      valid: false,
      error: result.error,
      message: result.errorMessage,
    });
  }

  return res.json({
    valid: true,
    iban: result.cleanedIban,
    country: result.countryInfo.countryName,
    sepa: result.countryInfo.isSepa,
  });
});

Vue

<script setup lang="ts">
import { ref, computed } from "vue";
import { validate } from "iban-validate";

const iban = ref("");
const result = computed(() =>
  iban.value.replace(/\s/g, "").length >= 15 ? validate(iban.value) : null,
);
</script>

<template>
  <div>
    <input v-model="iban" placeholder="DE89 3704 0044 0532 0130 00" />
    <p v-if="result?.isValid" style="color: green">
      ✓ Valid · {{ result.countryInfo.countryName }}
    </p>
    <p v-else-if="result" style="color: red">✗ {{ result.errorMessage }}</p>
  </div>
</template>

API reference

isValid

function isValid(iban: string, options?: { countryCca2?: string }): boolean;

Returns true if iban passes all validation checks.

  • Spaces are stripped and the string is uppercased automatically.
  • If countryCca2 is provided the IBAN country code must match it (case-insensitive).
  • For detailed failure information use validate instead.
isValid("DE89 3704 0044 0532 0130 00"); // true
isValid("de89370400440532013000"); // true
isValid("DE89370400440532013000", { countryCca2: "DE" }); // true
isValid("DE89370400440532013000", { countryCca2: "fr" }); // false
isValid(""); // false

validate

function validate(
  iban: string,
  options?: { countryCca2?: string },
): IbanValidationResult;

Returns an IbanValidationResult with full detail.

Validation runs in this exact order, stopping at the first failure:

| Step | Check | | ---- | -------------------------------------------------- | | 1 | Strip spaces, uppercase | | 2 | Not empty | | 3 | At least 4 characters | | 4 | Only A–Z and 0–9 characters | | 5 | Country code exists in the registry | | 6 | Country code matches countryCca2 (if provided) | | 7 | Length matches the expected length for the country | | 8 | mod-97 checksum equals 1 |


IbanValidationResult

A discriminated union — TypeScript narrows the type automatically in if blocks:

type IbanValidationResult =
  | {
      isValid: true;
      cleanedIban: string;
      error: undefined;
      errorMessage: undefined;
      countryInfo: IbanCountryInfo; // always defined when valid
    }
  | {
      isValid: false;
      cleanedIban: string;
      error: IbanValidationError; // always defined when invalid
      errorMessage: string;
      countryInfo: IbanCountryInfo | undefined; // defined if country was recognised
    };
const r = validate("GB29NWBK60161331926819");

if (r.isValid) {
  // TypeScript knows these are defined — no optional chaining needed
  r.countryInfo.countryName; // 'United Kingdom'
  r.countryInfo.isSepa; // true
  r.cleanedIban; // 'GB29NWBK60161331926819'
} else {
  r.error; // IbanValidationError
  r.errorMessage; // human-readable string
}

IbanValidationError

type IbanValidationError =
  | "emptyInput" // input was empty or whitespace only
  | "tooShort" // fewer than 4 characters after stripping spaces
  | "invalidCharacters" // contains characters outside A–Z, 0–9
  | "unknownCountry" // first two characters not a recognised IBAN country code
  | "countryMismatch" // country code doesn't match countryCca2 constraint
  | "invalidLength" // length doesn't match the expected length for the country
  | "checksumFailed"; // mod-97 checksum ≠ 1 — the IBAN number itself is wrong
const result = validate(userInput);

if (!result.isValid) {
  switch (result.error) {
    case "unknownCountry":
      showCountryPicker();
      break;
    case "invalidLength":
      showHint(`Should be ${result.countryInfo?.ibanLength} characters`);
      break;
    case "checksumFailed":
      showHint("Double-check your IBAN — there may be a typo");
      break;
    default:
      showError(result.errorMessage);
  }
}

getSupportedCountries

function getSupportedCountries(): string[];

Returns a sorted array of all supported two-letter country codes (116 total).

const codes = getSupportedCountries();
// ['AD', 'AE', 'AL', 'AM', 'AO', ..., 'YE']
codes.length; // 116

getCountryInfo

function getCountryInfo(countryCode: string): IbanCountryInfo | undefined;

Returns IbanCountryInfo for the given code, or undefined if unsupported. Case-insensitive.

getCountryInfo("DE"); // { countryCode: 'DE', countryName: 'Germany', ibanLength: 22, ... }
getCountryInfo("de"); // same result
getCountryInfo("US"); // undefined — USA does not use IBAN

IbanCountryInfo

interface IbanCountryInfo {
  readonly countryCode: string; // 'DE'
  readonly countryName: string; // 'Germany'
  readonly ibanLength: number; // 22
  readonly isSepa: boolean; // true
  readonly isExperimental: boolean; // false
  readonly example: string; // 'DE75512108001245126199'
}

Other helpers

// All SEPA countries sorted by code
function getSepaCountries(): IbanCountryInfo[];

// All non-SEPA countries sorted by code
function getNonSepaCountries(): IbanCountryInfo[];

// Experimental / partial-IBAN countries (not yet in ISO 13616)
function getExperimentalCountries(): IbanCountryInfo[];

// Expected IBAN length for a country code, or undefined if unsupported
function getExpectedLength(countryCode: string): number | undefined;
getExpectedLength("NO"); // 15 (shortest IBAN)
getExpectedLength("RU"); // 33 (longest IBAN)
getExpectedLength("US"); // undefined

getSepaCountries().length; // 40
getExperimentalCountries().length; // 22

Error handling patterns

Simple service function

import { validate } from "iban-validate";

// Returns null on success, error message on failure
function validateBeneficiaryIban(iban: string, country: string): string | null {
  const result = validate(iban, { countryCca2: country });
  return result.isValid ? null : result.errorMessage;
}

Throwing on invalid input

import { validate } from "iban-validate";

function processPayment(iban: string) {
  const result = validate(iban);
  if (!result.isValid) {
    throw new Error(`Invalid IBAN: ${result.errorMessage}`);
  }
  // use result.cleanedIban safely from here
  const cleanIban = result.cleanedIban;
}

Filtering a list of IBANs

import { isValid } from "iban-validate";

const ibans = [
  "DE89 3704 0044 0532 0130 00",
  "INVALID",
  "GB29 NWBK 6016 1331 9268 19",
  "FR76 3000 6000 0112 3456 7890 189",
];

const valid = ibans.filter(isValid);
const invalid = ibans.filter((i) => !isValid(i));

With async/await in a payment service

import { validate } from "iban-validate";

async function submitPayment(payload: {
  iban: string;
  amount: number;
  currency: string;
}) {
  const result = validate(payload.iban);

  if (!result.isValid) {
    return {
      success: false,
      error: result.error,
      message: result.errorMessage,
    };
  }

  const response = await fetch("/api/payments", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      iban: result.cleanedIban, // always use the cleaned version
      country: result.countryInfo.countryCode,
      isSepa: result.countryInfo.isSepa,
      amount: payload.amount,
      currency: payload.currency,
    }),
  });

  return response.json();
}

CommonJS usage

const { isValid, validate, getCountryInfo } = require("iban-validate");

console.log(isValid("DE89 3704 0044 0532 0130 00")); // true

How IBAN validation works

An IBAN (International Bank Account Number) is validated using the ISO 7064 MOD-97-10 algorithm. Here are the five steps:

1. Clean the input Strip all spaces and convert to uppercase. 'de89 3704''DE893704'.

2. Look up the country The first two characters are the ISO 3166-1 alpha-2 country code. Check it against the registry and verify the total length matches the country's fixed IBAN length.

3. Rearrange Move the first four characters (country code + check digits) to the end. 'DE89370400440532013000''370400440532013000DE89'

4. Convert letters to numbers Replace every letter with its numeric equivalent: A=10, B=11Z=35. '...DE89''...131489'

5. Compute mod 97 Interpret the resulting digit string as a large integer and compute the remainder when divided by 97. If the remainder equals 1, the IBAN is valid.

The digit string can be up to 34+ characters — far too large for JavaScript's Number (MAX_SAFE_INTEGER ≈ 9×10¹⁵, but the IBAN number can reach ~10³⁴). This package processes it in 7-digit chunks, carrying the remainder forward, keeping every intermediate value safely within the safe integer range without needing BigInt.


Supported countries

Official ISO 13616 countries (94)

Source: iban.com/structure, updated 30 March 2026.

| Code | Country | Length | SEPA | | ---- | ---------------------- | :----: | :--: | | AD | Andorra | 24 | ✓ | | AE | United Arab Emirates | 23 | | | AL | Albania | 28 | | | AM | Armenia | 28 | | | AT | Austria | 20 | ✓ | | AZ | Azerbaijan | 28 | | | BA | Bosnia and Herzegovina | 20 | | | BE | Belgium | 16 | ✓ | | BH | Bahrain | 22 | | | BI | Burundi | 27 | | | BG | Bulgaria | 22 | ✓ | | BR | Brazil | 29 | | | BY | Belarus | 28 | | | CH | Switzerland | 21 | ✓ | | CR | Costa Rica | 22 | | | CY | Cyprus | 28 | ✓ | | CZ | Czech Republic | 24 | ✓ | | DE | Germany | 22 | ✓ | | DJ | Djibouti | 27 | | | DK | Denmark | 18 | ✓ | | DO | Dominican Republic | 28 | | | EE | Estonia | 20 | ✓ | | EG | Egypt | 29 | | | ES | Spain | 24 | ✓ | | FI | Finland | 18 | ✓ | | FK | Falkland Islands | 18 | | | FO | Faroe Islands | 18 | | | FR | France | 27 | ✓ | | GB | United Kingdom | 22 | ✓ | | GE | Georgia | 22 | | | GI | Gibraltar | 23 | ✓ | | GL | Greenland | 18 | | | GR | Greece | 27 | ✓ | | GT | Guatemala | 28 | | | HN | Honduras | 28 | | | HR | Croatia | 21 | ✓ | | HU | Hungary | 28 | ✓ | | IE | Ireland | 22 | ✓ | | IL | Israel | 23 | | | IQ | Iraq | 23 | | | IS | Iceland | 26 | ✓ | | IT | Italy | 27 | ✓ | | JO | Jordan | 30 | | | KG | Kyrgyzstan | 26 | | | KW | Kuwait | 30 | | | KZ | Kazakhstan | 20 | | | LB | Lebanon | 28 | | | LC | Saint Lucia | 32 | | | LI | Liechtenstein | 21 | ✓ | | LT | Lithuania | 20 | ✓ | | LU | Luxembourg | 20 | ✓ | | LV | Latvia | 21 | ✓ | | LY | Libya | 25 | | | MC | Monaco | 27 | ✓ | | MD | Moldova | 24 | ✓ | | ME | Montenegro | 22 | ✓ | | MK | North Macedonia | 19 | ✓ | | MN | Mongolia | 20 | | | MR | Mauritania | 27 | | | MT | Malta | 31 | ✓ | | MU | Mauritius | 30 | | | NI | Nicaragua | 28 | | | NL | Netherlands | 18 | ✓ | | NO | Norway | 15 | ✓ | | OM | Oman | 23 | | | PK | Pakistan | 24 | | | PL | Poland | 28 | ✓ | | PS | Palestine | 29 | | | PT | Portugal | 25 | ✓ | | QA | Qatar | 29 | | | RO | Romania | 24 | ✓ | | RS | Serbia | 22 | ✓ | | RU | Russia | 33 | | | SA | Saudi Arabia | 24 | | | SC | Seychelles | 31 | | | SD | Sudan | 18 | | | SE | Sweden | 24 | ✓ | | SI | Slovenia | 19 | ✓ | | SK | Slovakia | 24 | ✓ | | SM | San Marino | 27 | ✓ | | SO | Somalia | 23 | | | ST | Sao Tome and Principe | 25 | | | SV | El Salvador | 28 | | | TJ | Tajikistan | 22 | | | TL | Timor-Leste | 23 | | | TM | Turkmenistan | 26 | | | TN | Tunisia | 24 | | | TR | Turkey | 26 | | | UA | Ukraine | 29 | | | UZ | Uzbekistan | 28 | | | VA | Vatican City | 22 | ✓ | | VG | British Virgin Islands | 24 | | | XK | Kosovo | 20 | | | YE | Yemen | 30 | |

Experimental / partial IBAN countries (22)

These countries have adopted an IBAN-like format locally but are not yet registered in the official ISO 13616 SWIFT registry. Their formats may change. In code these entries have isExperimental: true.

Note: Use experimental IBANs with care in production. Consider showing a disclaimer to your users for these countries.

| Code | Country | Length | | ---- | ------------------------ | :----: | | AO | Angola | 25 | | BF | Burkina Faso | 28 | | BJ | Benin | 28 | | CF | Central African Republic | 27 | | CG | Congo | 27 | | CI | Ivory Coast | 28 | | CM | Cameroon | 27 | | CV | Cape Verde | 25 | | DZ | Algeria | 24 | | GA | Gabon | 27 | | GQ | Equatorial Guinea | 27 | | GW | Guinea-Bissau | 25 | | IR | Iran | 26 | | KM | Comoros | 27 | | MA | Morocco | 28 | | MG | Madagascar | 27 | | ML | Mali | 28 | | MZ | Mozambique | 25 | | NE | Niger | 28 | | SN | Senegal | 28 | | TD | Chad | 27 | | TG | Togo | 28 |


Contributing

Contributions are welcome — especially updates to the country registry.

To add or update a country:

  1. Edit src/ibanData.ts with the new or corrected entry.
  2. Add a corresponding test case in test/index.test.ts.
  3. Run npm test to confirm all tests pass.
  4. Open a pull request.

Registry source: iban.com/structure (updated 30 March 2026).

Reporting bugs: Open an issue at github.com/khrisbreezy/iban-validation/issues and include the IBAN (or a structurally equivalent fake one), the country, and the result you expected vs what you got.


Related

  • iban_validator — the Dart / Flutter version of this package, published on pub.dev.

License

MIT © 2026 Oyinlola Abolarin