Every credit card number has a built-in error-detection mechanism called the Luhn algorithm (also known as "modulus 10"). It catches accidental typos — swapped digits, single-digit errors, and most transposition mistakes — before a number ever reaches your payment processor.
This article explains how it works step by step, shows you a clean JavaScript implementation, and covers brand detection so you can build a complete card validator.
How the Luhn Algorithm Works
The algorithm operates on the card number's digits from right to left:
- Start from the rightmost digit (the check digit) and move left
- Double every second digit (starting from the second-to-last)
- If doubling produces a number > 9, subtract 9
- Sum all digits
- If the total is divisible by 10, the number is valid
Walk-Through: 4111 1111 1111 1111
This is Visa's standard test number. Let's trace through it:
Original: 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Double alt: 8 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1
Sum: 8+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1 = 30
30 % 10 === 0 → Valid
Now change one digit — 4111 1111 1111 1112:
Sum: 31
31 % 10 !== 0 → Invalid
A single typo changes the checksum. That's the point.
JavaScript Implementation
function luhn(cardNumber) {
const digits = cardNumber.replace(/\D/g, '');
let sum = 0;
let alternate = false;
// Walk from right to left
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
luhn('4111111111111111'); // true (Visa test)
luhn('4111111111111112'); // false (one digit off)
luhn('5500000000000004'); // true (Mastercard test)
luhn('378282246310005'); // true (Amex test)
That's 15 lines. No dependencies, no libraries. It runs in under 1 microsecond.
Detecting the Card Brand
Card brands are identified by their prefix (BIN range) and length:
| Brand | Prefix | Length |
|---|---|---|
| Visa | 4 | 13, 16, 19 |
| Mastercard | 51-55, 2221-2720 | 16 |
| Amex | 34, 37 | 15 |
| Discover | 6011, 644-649, 65 | 16-19 |
| JCB | 3528-3589 | 16-19 |
| UnionPay | 62 | 16-19 |
| Diners Club | 300-305, 36, 38 | 14-19 |
function detectBrand(cardNumber) {
const d = cardNumber.replace(/\D/g, '');
if (/^4/.test(d)) return 'Visa';
if (/^5[1-5]/.test(d) || /^2[2-7]/.test(d)) return 'Mastercard';
if (/^3[47]/.test(d)) return 'Amex';
if (/^6(?:011|4[4-9]|5)/.test(d)) return 'Discover';
if (/^35(?:2[89]|[3-8])/.test(d)) return 'JCB';
if (/^62/.test(d)) return 'UnionPay';
return 'Unknown';
}
Complete Validator
Combining Luhn check, brand detection, and length validation:
function validateCard(input) {
const digits = input.replace(/\D/g, '');
if (digits.length < 13 || digits.length > 19) {
return { valid: false, reason: 'Card number must be 13-19 digits' };
}
if (!luhn(digits)) {
return { valid: false, reason: 'Invalid checksum (Luhn check failed)' };
}
const brand = detectBrand(digits);
const lastFour = digits.slice(-4);
const bin = digits.substring(0, 6);
return {
valid: true,
brand,
last_four: lastFour,
bin,
formatted: digits.replace(/(.{4})/g, '$1 ').trim(),
};
}
validateCard('4111-1111-1111-1111');
// { valid: true, brand: "Visa", last_four: "1111", bin: "411111",
// formatted: "4111 1111 1111 1111" }
What Luhn Doesn't Catch
The Luhn algorithm only checks for accidental errors. It does not:
- Verify the card exists — you need a payment processor for that
- Check if the card is expired — validate expiry separately
- Detect stolen numbers — that's fraud detection, not validation
-
Catch all transposition errors — swapping
09and90passes Luhn
Think of Luhn as a fast pre-filter. It rejects obvious mistakes before you make an API call to Stripe or your payment gateway. This saves you money (each processor call costs something) and gives users instant feedback.
Using an API Instead
If you want Luhn validation + brand detection + formatting without writing the code yourself:
const res = await fetch(
'https://datacheck.dev/api/validate?input=4111111111111111&type=credit_card'
);
const data = await res.json();
// {
// valid: true,
// formatted: "4111 1111 1111 1111",
// details: {
// brand: "Visa",
// last_four: "1111",
// bin: "411111",
// type: "credit"
// }
// }
Or with the npm package:
npm install datacheck-api
import { validateCard } from "datacheck-api";
const result = await validateCard("4111111111111111");
Test Card Numbers
Use these for testing. They all pass Luhn but aren't real cards:
| Brand | Number |
|---|---|
| Visa | 4111 1111 1111 1111 |
| Mastercard | 5500 0000 0000 0004 |
| Amex | 3782 822463 10005 |
| Discover | 6011 1111 1111 1117 |
| JCB | 3530 1113 3330 0000 |
Wrapping Up
The Luhn algorithm is one of the most useful 15-line functions you'll ever write. It catches typos instantly, works offline, and costs nothing to run. Pair it with brand detection and you've got a complete client-side card validator.
For production apps, use it as a pre-filter before calling your payment processor — or use an API like DataCheck that handles everything in one call.
Top comments (0)