DEV Community

David
David

Posted on

The Luhn Algorithm Explained: How Credit Card Numbers Are Validated

You enter a credit card number into a checkout form. Before any network request fires, the form already knows your number is invalid. No API call, no bank lookup — just instant red text.

How? The Luhn algorithm — a 70-year-old checksum formula that validates card numbers, IMEI codes, and national IDs. It's one of those things every developer uses but few actually understand.

Let's fix that.


What Is the Luhn Algorithm?

The Luhn algorithm (also called the Luhn formula or modulus 10 algorithm) was created by IBM scientist Hans Peter Luhn in 1954. U.S. Patent 2,950,048. Its purpose: detect accidental errors in identification numbers.

It catches:

  • Single-digit typos (typing a 5 instead of a 6)
  • Adjacent transposition errors (swapping 3443)
  • Most random input

It does not provide security or encryption. It's purely an error-detection mechanism.


How It Works (Step by Step)

Let's validate the number: 4539 1488 0343 6467

Step 1: Start from the rightmost digit

Write out all digits:

4  5  3  9  1  4  8  8  0  3  4  3  6  4  6  7
Enter fullscreen mode Exit fullscreen mode

Step 2: Double every second digit from the right

Starting from the second-to-last digit (moving left), double every other digit:

Position:  16  15  14  13  12  11  10  9   8   7   6   5   4   3   2   1
Digit:      4   5   3   9   1   4   8   8   0   3   4   3   6   4   6   7
Double?:    ×2      ×2      ×2      ×2      ×2      ×2      ×2      ×2
Result:     8   5   6   9   2   4  16   8   0   3   8   3  12   4  12   7
Enter fullscreen mode Exit fullscreen mode

Step 3: If any doubled value > 9, subtract 9

8  5  6  9  2  4  7  8  0  3  8  3  3  4  3  7
Enter fullscreen mode Exit fullscreen mode

(16 → 7, 12 → 3, 12 → 3)

Step 4: Sum all digits

8 + 5 + 6 + 9 + 2 + 4 + 7 + 8 + 0 + 3 + 8 + 3 + 3 + 4 + 3 + 7 = 80
Enter fullscreen mode Exit fullscreen mode

Step 5: Check if sum mod 10 === 0

80 % 10 === 0 ✅ VALID
Enter fullscreen mode Exit fullscreen mode

That's it. If the total is divisible by 10, the number passes Luhn validation.


The Code

JavaScript

function isValidLuhn(number) {
  const digits = String(number).replace(/\D/g, '');
  let sum = 0;
  let alternate = false;

  for (let i = digits.length - 1; i >= 0; i--) {
    let n = parseInt(digits[i], 10);

    if (alternate) {
      n *= 2;
      if (n > 9) n -= 9;
    }

    sum += n;
    alternate = !alternate;
  }

  return sum % 10 === 0;
}

// Test it
console.log(isValidLuhn('4539148803436467')); // true
console.log(isValidLuhn('1234567890123456')); // false
Enter fullscreen mode Exit fullscreen mode

Python

def is_valid_luhn(number: str) -> bool:
    digits = [int(d) for d in str(number) if d.isdigit()]
    digits.reverse()

    total = 0
    for i, d in enumerate(digits):
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d

    return total % 10 == 0

print(is_valid_luhn("4539148803436467"))  # True
print(is_valid_luhn("1234567890123456"))  # False
Enter fullscreen mode Exit fullscreen mode

Go

func isValidLuhn(number string) bool {
    sum := 0
    alternate := false

    for i := len(number) - 1; i >= 0; i-- {
        n := int(number[i] - '0')
        if alternate {
            n *= 2
            if n > 9 {
                n -= 9
            }
        }
        sum += n
        alternate = !alternate
    }

    return sum%10 == 0
}
Enter fullscreen mode Exit fullscreen mode

Generating a Valid Number (Check Digit Calculation)

What if you need to generate a number that passes Luhn? You compute the check digit — the last digit that makes the whole thing valid.

function generateCheckDigit(partialNumber) {
  const digits = String(partialNumber).replace(/\D/g, '');
  // Append a 0 as placeholder
  const withZero = digits + '0';

  let sum = 0;
  let alternate = true; // starts true because check digit is at position 1

  for (let i = withZero.length - 1; i >= 0; i--) {
    let n = parseInt(withZero[i], 10);
    if (alternate && i !== withZero.length - 1) {
      // Skip the placeholder itself
    }
    if (alternate) {
      n *= 2;
      if (n > 9) n -= 9;
    }
    sum += n;
    alternate = !alternate;
  }

  return (10 - (sum % 10)) % 10;
}
Enter fullscreen mode Exit fullscreen mode

This is exactly how payment processors and test data generators work — pick a valid BIN prefix, generate random middle digits, then compute the check digit.


Anatomy of a Credit Card Number

Every card number has a structure:

┌─────────┬──────────────────┬─────┐
│   BIN   │  Account Number  │Check│
│ (6 dig) │   (variable)     │Digit│
└─────────┴──────────────────┴─────┘
Enter fullscreen mode Exit fullscreen mode
  • BIN (Bank Identification Number): First 6 digits identify the issuer
  • Account Number: Unique to the cardholder
  • Check Digit: Last digit, computed via Luhn
First Digit Network
3 American Express (+ Diners Club)
4 Visa
5 Mastercard
6 Discover

The total length varies: Visa uses 16 digits, Amex uses 15, and some Maestro cards go up to 19.


Why Developers Need Test Card Numbers

If you're building anything that handles payments, you need test card numbers constantly:

  • Unit testing checkout flows and payment forms
  • Integration testing with payment gateways (Stripe, Adyen, PayPal)
  • QA validation of form masks, formatting, and error handling
  • Load testing payment APIs with realistic data
  • Demo environments that need to look real without using real cards

Most payment processors provide a handful of test numbers:

Provider Test Number
Stripe 4242 4242 4242 4242
PayPal Sandbox 4032 0361 9834 6245
Adyen 5500 0000 0000 0004

But when you need hundreds of valid numbers across different networks and BINs for thorough testing? That's where a proper test card number generator helps.

namso.io generates Luhn-valid card numbers from any BIN prefix. Pick a BIN, choose the quantity, get numbers that pass frontend validation — perfect for testing without touching real financial data.


Where Else Luhn Shows Up

Credit cards get the spotlight, but Luhn validates many other identifiers:

Identifier Example
IMEI numbers 15-digit mobile device IDs
Canadian Social Insurance Numbers 9-digit SINs
Israeli ID numbers National IDs
Greek Social Security AMKA numbers
Some European VAT numbers Country-specific
Provider IDs (NPI) US healthcare

The algorithm is the same everywhere — only the length and prefix rules change.


Common Mistakes

1. Using Luhn as "security"

Luhn is a checksum, not encryption. A number passing Luhn doesn't mean it's a real card. It means it's formatted correctly.

2. Forgetting to strip non-digits

Card numbers often come with spaces or dashes: 4539-1488-0343-6467. Always sanitize before validating:

const clean = input.replace(/[\s-]/g, '');
Enter fullscreen mode Exit fullscreen mode

3. Starting the doubling from the wrong end

Always start from the rightmost digit and move left. Starting from the left gives wrong results.

4. Not handling the "subtract 9" step

If a doubled digit equals 10 or more, you subtract 9 (not sum the individual digits, though both methods give the same result).


Quick Reference

Step Action
1 Strip non-digit characters
2 Reverse the digits
3 Double every second digit (index 1, 3, 5...)
4 If doubled value > 9, subtract 9
5 Sum all digits
6 Valid if sum % 10 === 0

Wrapping Up

The Luhn algorithm is elegant in its simplicity — a few arithmetic operations that catch the vast majority of accidental input errors. It's been doing this job since 1954, and it's still embedded in every checkout form, every card validator, and every payment SDK you've ever used.

For generating Luhn-valid test card numbers during development, check out namso.io — BIN-level control, bulk generation, all client-side.

Next time you see that instant "invalid card number" error, you'll know exactly what's happening under the hood.


This is part of the Developer Tools Deep Dives series — practical guides to the tools and algorithms developers use every day.

Top comments (0)