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
5instead of a6) -
Adjacent transposition errors (swapping
34→43) - 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
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
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
(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
Step 5: Check if sum mod 10 === 0
80 % 10 === 0 ✅ VALID
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
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
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
}
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;
}
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│
└─────────┴──────────────────┴─────┘
- 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, '');
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)