Guide · Algorithms · Reference

Check Digit Algorithms Explained — Luhn, MOD-97, MOD-11 and More

Every major identifier — from credit cards to IBANs to vehicle identification numbers — includes a check digit computed by a specific algorithm. This guide covers all the algorithms you'll encounter in the real world, with formulas, worked examples, and practical code.

1. Why check digits exist

When a human types an identifier — a credit card number, an IBAN, a tax ID — there's roughly a 1 in 10 chance of mistyping a single digit. Transposing two adjacent digits is even more common. A check digit is an extra digit (or character) computed from the rest of the identifier using a mathematical formula.

Before sending the number to a remote system, you recompute the check digit and compare it with the one provided. If they don't match, the number is wrong — guaranteed. This saves a network round-trip and prevents costly errors in banking, finance, and logistics.

ℹ️Check digits protect against accidental errors — not fraud. They are not a cryptographic signature. Anyone who knows the algorithm can compute a valid check digit for any payload.

Different algorithms offer different detection capabilities. The simplest (sum mod 10) catches all single-digit errors. More sophisticated algorithms (like Luhn, MOD-97, or Verhoeff) also catch transposition errors and other common mistake patterns. The choice of algorithm depends on the length of the identifier and the acceptable error rate.


2. Luhn algorithm (mod-10)

Invented by IBM scientist Hans Peter Luhn in 1954, this is the most widely used check digit algorithm in the world. It's used for credit card numbers, ISIN codes, and IMEI numbers.

How it works

Step 1. Starting from the rightmost digit (the check digit) and moving left, double every second digit.

Step 2. If doubling produces a number greater than 9, subtract 9 (equivalent to summing the two digits: 14 → 1+4 = 5, or 14−9 = 5).

Step 3. Sum all digits (both doubled and undoubled).

Step 4. If sum % 10 === 0, the number is valid.

Worked example — Visa 4539 1488 0343 6467

Step4539148803436467
Double?x2-x2-x2-x2-x2-x2-x2-x2-
After8569247803833437

Sum: 8+5+6+9+2+4+7+8+0+3+8+3+3+4+3+7 = 80. Since 80 % 10 = 0, the number is valid.

For alphanumeric identifiers (ISIN)

ISIN codes contain letters. Before applying Luhn, each letter is replaced with its numeric value: A=10, B=11, ..., Z=35. The resulting digit string is then processed with the standard Luhn algorithm. For example, ISIN US0378331005 becomes 3028037833100 + check digit 5.

// Luhn algorithm — works for credit cards, IMEI, ISIN (after letter→digit conversion)
function luhnCheck(digits: string): boolean {
  let sum = 0;
  let shouldDouble = false;

  for (let i = digits.length - 1; i >= 0; i--) {
    let d = parseInt(digits[i], 10);
    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
    shouldDouble = !shouldDouble;
  }

  return sum % 10 === 0;
}

Where it's used

IdentifierFormatCheck digit position
Credit Card13–19 digitsLast digit
ISIN2 letters + 9 alnum + 1 digitLast digit (after letter→digit conversion)
IMEI15 digitsLast digit

3. MOD-97 algorithm (ISO 7064)

MOD-97 is used by IBAN (International Bank Account Number) and LEI (Legal Entity Identifier). It provides two check digits instead of one, which gives it much stronger error detection — it catches 99.97% of all errors, including all single-digit errors, all transpositions, and most jump transpositions.

How it works (IBAN example)

Step 1. Move the first 4 characters (country code + 2 check digits) to the end.

GB29 NWBK 6016 1331 9268 19 → NWBK 6016 1331 9268 19 GB29

Step 2. Replace each letter with its numeric value: A=10, B=11, ..., Z=35.

N=23, W=32, B=11, K=20 → 2332112060161331926819161129

Step 3. Compute this (very large) number modulo 97. Process in chunks of 9 digits to avoid integer overflow.

Step 4. If the remainder is 1, the IBAN is valid.

Chunked modulo computation

The numeric string can be 30+ digits — far beyond what a 64-bit integer can hold. The standard technique is to process the string in chunks:

// MOD-97 check — used for IBAN and LEI validation
function mod97(digitString: string): number {
  let remainder = 0;
  for (let i = 0; i < digitString.length; i++) {
    remainder = (remainder * 10 + parseInt(digitString[i], 10)) % 97;
  }
  return remainder;
}

// For IBAN: rearrange, convert letters, then check mod97 === 1
function validateIBAN(iban: string): boolean {
  const cleaned = iban.replace(/\s/g, '').toUpperCase();
  // Move first 4 chars to end
  const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
  // Replace letters with numbers (A=10, B=11, ..., Z=35)
  const digits = rearranged.replace(/[A-Z]/g, (ch) =>
    String(ch.charCodeAt(0) - 55)
  );
  return mod97(digits) === 1;
}

LEI — same algorithm, different format

The LEI (ISO 17442) is a 20-character identifier: 4 digits (LOU prefix) + 14 alphanumeric characters + 2 check digits. The check digit calculation is identical to IBAN — move the first 4 characters to the end, convert letters to numbers, and verify that mod-97 equals 1.

MOD-97 uses two check digits, which makes it significantly more powerful than single-digit algorithms. With one check digit you can catch ~90% of errors; with two, you catch ~99.97%.

4. Weighted MOD-11

Several identifiers use a weighted sum with modulo 11. Each digit is multiplied by a weight that depends on its position. The result is taken mod 11, and if the remainder is 10, the check character is 'X' (representing 10 in a single character).

ISBN-10

The classic 10-digit ISBN uses descending weights 10, 9, 8, ..., 2 for the first 9 digits. The check digit makes the weighted sum divisible by 11.

Example: ISBN-10 0-306-40615-2

Digit0306406152
Weight10987654321
Product027042240243102

Sum: 0+27+0+42+24+0+24+3+10+2 = 132. Since 132 % 11 = 0, the ISBN is valid.

// ISBN-10 check digit validation
function validateISBN10(isbn: string): boolean {
  const digits = isbn.replace(/[\s-]/g, '');
  if (digits.length !== 10) return false;

  let sum = 0;
  for (let i = 0; i < 10; i++) {
    const ch = digits[i];
    const val = ch === 'X' || ch === 'x' ? 10 : parseInt(ch, 10);
    sum += val * (10 - i);
  }

  return sum % 11 === 0;
}

ISSN

The 8-digit ISSN uses weights 8, 7, 6, 5, 4, 3, 2, 1. Same principle — weighted sum mod 11 must equal 0. The last character can be 'X' (= 10).

// ISSN check digit validation
function validateISSN(issn: string): boolean {
  const digits = issn.replace(/[\s-]/g, '');
  if (digits.length !== 8) return false;

  let sum = 0;
  for (let i = 0; i < 8; i++) {
    const ch = digits[i];
    const val = ch === 'X' || ch === 'x' ? 10 : parseInt(ch, 10);
    sum += val * (8 - i);
  }

  return sum % 11 === 0;
}

REGON (Poland)

The Polish REGON uses mod-11 with non-sequential weights. The 9-digit version uses weights [8, 9, 2, 3, 4, 5, 6, 7]. The 14-digit version first validates the 9-digit prefix, then applies a second set of weights [2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8] to all 13 non-check digits. If the remainder is 10, the check digit is 0.

// REGON-9 check digit validation
function validateREGON9(regon: string): boolean {
  if (regon.length !== 9) return false;

  const weights = [8, 9, 2, 3, 4, 5, 6, 7];
  let sum = 0;
  for (let i = 0; i < 8; i++) {
    sum += parseInt(regon[i], 10) * weights[i];
  }

  let check = sum % 11;
  if (check === 10) check = 0;

  return check === parseInt(regon[8], 10);
}

5. Weighted MOD-10

Several identifiers use a weighted sum with modulo 10 — simpler than mod-11 (no 'X' needed) but still effective at catching common errors. Different identifiers use different weight sequences.

ISBN-13 / EAN-13 (alternating 1, 3)

ISBN-13 and EAN-13 use the same algorithm: multiply each of the first 12 digits by alternating weights of 1 and 3, sum the results, and the check digit is (10 - sum % 10) % 10.

Example: EAN-13 4006381333931

Digit4006381333931
Weight131313131313-
Product40018324193999-

Sum: 4+0+0+18+3+24+1+9+3+9+9+9 = 89. Check: (10 − 89 % 10) % 10 = 1. Matches the last digit.

// EAN-13 / ISBN-13 check digit validation
function validateEAN13(code: string): boolean {
  if (code.length !== 13) return false;

  let sum = 0;
  for (let i = 0; i < 12; i++) {
    sum += parseInt(code[i], 10) * (i % 2 === 0 ? 1 : 3);
  }

  const check = (10 - (sum % 10)) % 10;
  return check === parseInt(code[12], 10);
}

EAN-8 (alternating 3, 1)

Note the reversed starting weight: EAN-8 uses 3, 1, 3, 1, 3, 1, 3 for the first 7 digits — the opposite order from EAN-13. This is a common source of bugs when implementing both formats.

ABA Routing Number (repeating 3, 7, 1)

US bank routing numbers use the repeating weight sequence 3, 7, 1 for all 9 digits. The weighted sum mod 10 must equal 0.

// ABA Routing Number validation
function validateABA(routing: string): boolean {
  if (routing.length !== 9) return false;

  const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1];
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += parseInt(routing[i], 10) * weights[i];
  }

  return sum % 10 === 0;
}

PESEL (Poland — weights 1, 3, 7, 9, ...)

The Polish national ID number uses the weight pattern [1, 3, 7, 9] repeated, plus a final weight of 1 for the check digit itself. The weighted sum of all 11 digits mod 10 must equal 0.

// PESEL validation
function validatePESEL(pesel: string): boolean {
  if (pesel.length !== 11) return false;

  const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3, 1];
  let sum = 0;
  for (let i = 0; i < 11; i++) {
    sum += parseInt(pesel[i], 10) * weights[i];
  }

  return sum % 10 === 0;
}

6. VIN check digit (MOD-11 with transliteration)

Vehicle Identification Numbers use a unique combination of character transliteration and weighted mod-11 checksumming. The check digit sits at position 9 (the 9th character) and can be 0–9 or X.

Transliteration table

Each letter is converted to a numeric value. Note that I, O, Q are never used in VINs (they look like 1, 0, and 0/9).

LetterABCDEFGHJKLMNPRSTUVWXYZ
Value12345678123457923456789

Position weights

Each of the 17 positions has a fixed weight. Position 9 (the check digit itself) has weight 0 — it doesn't contribute to the sum:

Position:1234567891011121314151617
Weight:876543210098765432
// VIN check digit validation (mandatory for North American vehicles)
const VIN_TRANSLITERATION: Record<string, number> = {
  A:1, B:2, C:3, D:4, E:5, F:6, G:7, H:8,
  J:1, K:2, L:3, M:4, N:5, P:7, R:9,
  S:2, T:3, U:4, V:5, W:6, X:7, Y:8, Z:9,
};
const VIN_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];

function validateVINCheckDigit(vin: string): boolean {
  let sum = 0;
  for (let i = 0; i < 17; i++) {
    const ch = vin[i];
    const val = /\d/.test(ch) ? parseInt(ch, 10) : VIN_TRANSLITERATION[ch];
    sum += val * VIN_WEIGHTS[i];
  }

  const remainder = sum % 11;
  const expected = remainder === 10 ? 'X' : String(remainder);
  return vin[8] === expected;
}
⚠️The VIN check digit is mandatory only for North American (WMI starting with 1–5) and Chinese (WMI starting with L) vehicles. European VINs often have a valid check digit, but it is not guaranteed.

7. CUSIP check digit

CUSIP (Committee on Uniform Securities Identification Procedures) is a 9-character identifier for North American securities. Characters 1–8 are the payload; character 9 is the check digit. The algorithm is a variant of Luhn that handles alphanumeric input.

Step 1. Convert each character to a numeric value: digits 0–9 map to themselves, letters A–Z map to 10–35, and special characters *=36, @=37, #=38.

Step 2. For characters at odd positions (1-indexed: 2, 4, 6, 8), multiply the value by 2.

Step 3. For each product, sum both digits (e.g., 18 → 1+8 = 9). This is equivalent to Math.floor(v/10) + v%10.

Step 4. Sum all digit sums. Check digit = (10 - sum % 10) % 10.

// CUSIP check digit validation
function validateCUSIP(cusip: string): boolean {
  if (cusip.length !== 9) return false;

  let sum = 0;
  for (let i = 0; i < 8; i++) {
    const ch = cusip[i];
    let val: number;

    if (/\d/.test(ch)) val = parseInt(ch, 10);
    else if (/[A-Z]/.test(ch)) val = ch.charCodeAt(0) - 55; // A=10
    else if (ch === '*') val = 36;
    else if (ch === '@') val = 37;
    else if (ch === '#') val = 38;
    else return false;

    if (i % 2 === 1) val *= 2; // odd positions (0-indexed)
    sum += Math.floor(val / 10) + (val % 10);
  }

  const check = (10 - (sum % 10)) % 10;
  return check === parseInt(cusip[8], 10);
}

8. VAT number checksums

VAT identification numbers are where check digit algorithms get truly diverse. Each EU member state (and many non-EU countries) uses its own algorithm — some simple, some remarkably complex.

Germany — iterative MOD 11-10

German VAT uses an unusual iterative algorithm: start with a product of 10, then for each digit add it to the product, take mod 10 (with 0 → 10), double, and take mod 11. The final check digit makes the last product equal 1.

Czech Republic — weighted MOD 11

Uses weights [8, 7, 6, 5, 4, 3, 2] or [6, 5, 4, 3, 2, 1, 6, 5] depending on whether it's a legal entity (8 digits) or natural person (9–10 digits). Legal entity check digit is computed as (11 - sum % 11) % 10.

Brazil — CNPJ double weighted MOD 11

The 14-digit CNPJ uses two separate weighted MOD-11 calculations with different weight sequences. The first 12 digits produce check digit 1 (using weights 5,4,3,2,9,8,7,6,5,4,3,2), then all 13 digits produce check digit 2 (using weights 6,5,4,3,2,9,8,7,6,5,4,3,2). If the remainder is less than 2, the check digit is 0; otherwise it's 11 minus the remainder.

China — USCC weighted MOD 31

The Chinese Unified Social Credit Code uses modulo 31 with weights that are successive powers of 3 mod 31: [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28]. The check digit is a character from a 31-symbol alphabet (0–9 plus selected uppercase letters).

ℹ️Because every country has its own algorithm, we strongly recommend using a validation API rather than implementing 30+ check digit algorithms yourself. A single regex or length check will catch some errors but will miss many invalid numbers that happen to be the right length.

9. Comparison table

All algorithms at a glance — grouped by base modulus.

IdentifierAlgorithmModulusCheck digitsFormat
Credit CardLuhn101 (last)13–19 digits
ISINLuhn (alphanumeric)101 (last)12 chars
IMEILuhn101 (last)15 digits
IBANMOD-97972 (pos 3–4)15–34 chars
LEIMOD-97972 (last)20 chars
ISBN-10Weighted sum111 (last, 0–9/X)10 chars
ISSNWeighted sum111 (last, 0–9/X)8 chars
ISBN-13Weighted 1,3101 (last)13 digits
EAN-13Weighted 1,3101 (last)13 digits
EAN-8Weighted 3,1101 (last)8 digits
CUSIPLuhn variant101 (last)9 chars
VINWeighted + transliteration111 (pos 9, 0–9/X)17 chars
ABA RoutingWeighted 3,7,1101 (last)9 digits
PESELWeighted 1,3,7,9101 (last)11 digits
REGONWeighted111 (last)9 or 14 digits
VATCountry-specificvariesvariesvaries

10. What can check digits catch?

Not all algorithms are equal. Here's what each class of algorithm can detect:

Error typeSimple mod-10LuhnMOD-11MOD-97
Single digit substitution90%100%100%100%
Adjacent transposition (ab → ba)~89%~98%100%100%
Jump transposition (abc → cba)~89%~95%~91%~99.97%
Twin error (aa → bb)90%100%100%100%
Random error90%~90%~91%~99.97%
Luhn catches all single-digit errors and nearly all adjacent transpositions — making it ideal for identifiers that are typed by hand (credit cards, IMEI). MOD-97 with two check digits is the gold standard — used wherever the cost of an undetected error is highest (banking).

Single digit error

Changing any one digit to a different digit. Example: 4111111111111111 4111111111111211. All algorithms in this article catch 100% of single-digit errors (except simple mod-10 which catches 90%).

Transposition error

Swapping two adjacent digits. Example: 4111111111111111 4111111111111111 (not visible for equal digits, but 4539 4593 would be caught). Luhn catches nearly all of these because the doubling pattern makes the contribution of each position asymmetric.


Summary

Choosing an algorithm

If you're designing a new identifier format, consider: Luhn is the standard for numeric identifiers that humans type (cards, phone IMEIs). MOD-97 is the best choice when errors are costly and you can afford two check characters. Weighted MOD-11 with 'X' is a classic for book/serial identifiers where a single extra character is acceptable.

16+
Identifier types
validated by IsValid API
6
Algorithm families
Luhn, MOD-97, MOD-11, MOD-10, MOD-31, custom
<20ms
Response time
pure algorithmic checks

See also

Validate any identifier instantly

Free tier includes 100 API calls per day. All check digit algorithms are built in — no need to implement them yourself.