Skip to content

IBAN Validator — PHP source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
/**
 * Pure IBAN validation logic — ISO 13616 mod-97 checksum.
 *
 * Language: PHP
 * CosmoDev polyglot showcase port of the `iban-validator` tool.
 * Ported from src/lib/iban.ts — display source, part of CosmoDev's
 * polyglot tool pages.
 *
 * Normalizes the input (uppercase, strip whitespace/dashes), checks the
 * structural regex (country code + 2 check digits + 1-30 BBAN chars),
 * verifies the per-country length, then runs the ISO 13616 mod-97
 * checksum: move the first 4 chars to the end, map A=10..Z=35, and
 * confirm the resulting integer is congruent to 1 mod 97. PHP's PCRE
 * supports the lookahead used by the formatter, and the remainder stays
 * under 97 throughout the fold, so plain ints suffice. validate_iban
 * never throws — it reports failures through the 'error' key.
 */

declare(strict_types=1);

/**
 * Per-country IBAN lengths (ISO 13616) — a representative subset.
 */
function iban_lengths(): array
{
    return [
        'AL' => 28, 'AD' => 24, 'AT' => 20, 'AZ' => 28, 'BH' => 22, 'BY' => 28,
        'BE' => 16, 'BA' => 20, 'BR' => 29, 'BG' => 22, 'CR' => 22, 'HR' => 21,
        'CY' => 28, 'CZ' => 24, 'DK' => 18, 'DO' => 28, 'EE' => 20, 'FO' => 18,
        'FI' => 18, 'FR' => 27, 'GE' => 22, 'DE' => 22, 'GI' => 23, 'GR' => 27,
        'GL' => 18, 'GT' => 28, 'HU' => 28, 'IS' => 26, 'IE' => 22, 'IL' => 23,
        'IT' => 27, 'JO' => 30, 'KZ' => 20, 'XK' => 20, 'KW' => 30, 'LV' => 21,
        'LB' => 28, 'LI' => 21, 'LT' => 20, 'LU' => 20, 'MK' => 19, 'MT' => 31,
        'MR' => 27, 'MU' => 30, 'MC' => 27, 'MD' => 24, 'ME' => 22, 'NL' => 18,
        'NO' => 15, 'PK' => 24, 'PS' => 29, 'PL' => 28, 'PT' => 25, 'QA' => 29,
        'RO' => 24, 'LC' => 32, 'SM' => 27, 'ST' => 25, 'SA' => 24, 'RS' => 22,
        'SC' => 31, 'SK' => 24, 'SI' => 19, 'SG' => 19, 'ES' => 24, 'SE' => 24,
        'CH' => 21, 'TL' => 23, 'TN' => 24, 'TR' => 26, 'UA' => 29, 'AE' => 23,
        'GB' => 22, 'VG' => 24,
    ];
}

/**
 * mod-97 checksum over a CLEANED iban (uppercase, no spaces/dashes).
 */
function mod97_check(string $cleaned): bool
{
    // Move the first 4 chars (country + check) to the end.
    $rearranged = substr($cleaned, 4) . substr($cleaned, 0, 4);
    $numeric = '';
    for ($i = 0, $n = strlen($rearranged); $i < $n; $i++) {
        $code = ord($rearranged[$i]);
        if ($code >= 48 && $code <= 57) {
            $numeric .= $rearranged[$i];
        } elseif ($code >= 65 && $code <= 90) {
            $numeric .= (string)($code - 55); // A=10 .. Z=35
        } else {
            return false; // invalid character
        }
    }
    $rem = 0;
    for ($i = 0, $n = strlen($numeric); $i < $n; $i++) {
        $rem = ($rem * 10 + (ord($numeric[$i]) - 48)) % 97;
    }
    return $rem === 1;
}

/**
 * Validate an IBAN. Always returns an info array; never throws.
 *
 * @return array{input:string,cleaned:string,countryCode:?string,valid:bool,checksumOk:bool,lengthOk:bool,expectedLength:?int,formatted:string,error:?string}
 */
function validate_iban(string $input): array
{
    $cleaned = strtoupper(preg_replace('/[\s-]/', '', $input) ?? '');
    $cc = preg_match('/^[A-Z]{2}/', $cleaned) ? substr($cleaned, 0, 2) : null;

    $lengths = iban_lengths();
    $expected = ($cc !== null && isset($lengths[$cc])) ? $lengths[$cc] : null;

    $info = [
        'input' => $input,
        'cleaned' => $cleaned,
        'countryCode' => $cc,
        'valid' => false,
        'checksumOk' => false,
        'lengthOk' => false,
        'expectedLength' => $expected,
        'formatted' => trim(preg_replace('/(.{4})(?=.)/', '$1 ', $cleaned) ?? ''),
        'error' => null,
    ];

    if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/', $cleaned)) {
        $info['error'] = 'Invalid IBAN format.';
        return $info;
    }

    $lengthOk = $expected === null ? true : strlen($cleaned) === $expected;
    $checksumOk = mod97_check($cleaned);
    $info['checksumOk'] = $checksumOk;
    $info['lengthOk'] = $lengthOk;

    if (!$lengthOk) {
        $info['error'] = "Length should be {$expected} for {$cc}.";
        return $info;
    }
    if (!$checksumOk) {
        $info['error'] = 'Checksum failed.';
        return $info;
    }
    $info['valid'] = true;
    return $info;
}

// CLI demo: `php iban.php` validates a known-good reference IBAN.
var_export(validate_iban('GB82WEST12345698765432'));
echo PHP_EOL;

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →