How Apps Check If a Card Number Is Valid
Ever mistyped a credit card number and had a checkout form catch the mistake instantly - before you even clicked submit? That's not magic. It's a 60-year-old algorithm running silently in your browser.
In this post, we'll break down exactly how apps validate card numbers: the structure of a card, the Luhn algorithm, and how to implement it yourself in under 20 lines of JavaScript.
A Card Number Is Not Random
Before we talk about validation, it helps to understand what a card number actually encodes. A standard card number has three logical parts.
4539 1488 0343 6467
│ │ │
│ └─ Account └─ Check digit
└─ IIN / BIN (Issuer Identification Number)| Part | Digits | What it means |
|---|---|---|
| IIN / BIN | First 6–8 digits | Identifies the card network and issuing bank |
| Account number | Middle digits | Unique to your account |
| Check digit | Last digit | Used purely for validation |
For example:
4→ Visa51–55or2221–2720→ Mastercard34or37→ American Express6011→ Discover
A frontend can identify the card network by inspecting just the first few digits - which is why the card logo updates as you type.
Step 1: Identify the Network (BIN Check)
Apps use the Bank Identification Number (BIN) - the first 6–8 digits - to detect which network issued the card. This happens client-side, with no API call needed.
function getCardNetwork(cardNumber) {
const num = cardNumber.replace(/\s+/g, "");
if (/^4/.test(num)) return "Visa";
if (/^5[1-5]/.test(num) || /^2(2[2-9][1-9]|[3-6]\d{2}|7[01]\d|720)\d/.test(num))
return "Mastercard";
if (/^3[47]/.test(num)) return "Amex";
if (/^6011/.test(num)) return "Discover";
return "Unknown";
}
getCardNetwork("4539148803436467"); // → "Visa"
getCardNetwork("5412345678901234"); // → "Mastercard"Note: Real payment processors maintain much more comprehensive BIN databases, updated regularly as new card ranges are issued.
Step 2: Check the Length
Different networks use different card lengths:
| Network | Length |
|---|---|
| Visa | 13 or 16 digits |
| Mastercard | 16 digits |
| Amex | 15 digits |
| Discover | 16 digits |
A length check is fast and eliminates a large share of typos before we even run the heavier algorithm.
function isValidLength(cardNumber, network) {
const len = cardNumber.replace(/\s+/g, "").length;
const lengths = {
Visa: [13, 16],
Mastercard: [16],
Amex: [15],
Discover: [16],
};
return (lengths[network] ?? [16]).includes(len);
}Step 3: The Luhn Algorithm
This is the core of card validation. The Luhn algorithm (also called Mod 10) was invented by IBM scientist Hans Peter Luhn in 1954. It's a simple checksum formula designed to catch accidental digit errors - transpositions, missing digits, single wrong digits.
It does not verify that a card exists or has funds. It simply checks that the number is structurally plausible.
How it works
- Starting from the rightmost digit (the check digit), move left.
- Double every second digit (i.e., every digit at an even position from the right).
- If doubling produces a number > 9, subtract 9.
- Sum all digits.
- If the total is divisible by 10, the number is valid.
A worked example
Let's validate 4539 1488 0343 6467:
Original: 4 5 3 9 1 4 8 8 0 3 4 3 6 4 6 7
Position: 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
Double: ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓| Digit | Action | Result |
|---|---|---|
| 4 | ×2 = 8 | 8 |
| 5 | keep | 5 |
| 3 | ×2 = 6 | 6 |
| 9 | keep | 9 |
| 1 | ×2 = 2 | 2 |
| 4 | keep | 4 |
| 8 | ×2 = 16 → 16-9 | 7 |
| 8 | keep | 8 |
| 0 | ×2 = 0 | 0 |
| 3 | keep | 3 |
| 4 | ×2 = 8 | 8 |
| 3 | keep | 3 |
| 6 | ×2 = 12 → 12-9 | 3 |
| 4 | keep | 4 |
| 6 | ×2 = 12 → 12-9 | 3 |
| 7 | keep | 7 |
Sum = 8+5+6+9+2+4+7+8+0+3+8+3+3+4+3+7 = 80
80 ÷ 10 = 8 exactly → ✅ Valid
JavaScript Implementation
Here's a clean, complete Luhn implementation:
function luhn(cardNumber) {
const digits = cardNumber
.replace(/\D/g, "") // strip spaces and dashes
.split("")
.map(Number);
let sum = 0;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = digits[i];
// Double every second digit from the right (odd positions from 1)
if ((digits.length - i) % 2 === 0) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
}
return sum % 10 === 0;
}
luhn("4539 1488 0343 6467"); // → true
luhn("4539 1488 0343 6468"); // → false (last digit changed by 1)
luhn("1234 5678 9012 3456"); // → false (fake number)Putting It All Together
A real card validation function combines all three checks:
function validateCard(input) {
const number = input.replace(/\s+/g, "");
// 1. Only digits
if (!/^\d+$/.test(number)) {
return { valid: false, reason: "Contains non-numeric characters" };
}
// 2. Identify network
const network = getCardNetwork(number);
// 3. Check length
if (!isValidLength(number, network)) {
return { valid: false, reason: `Invalid length for ${network}` };
}
// 4. Luhn check
if (!luhn(number)) {
return { valid: false, reason: "Failed Luhn checksum" };
}
return { valid: true, network };
}
validateCard("4539 1488 0343 6467");
// → { valid: true, network: "Visa" }
validateCard("4539 1488 0343 6468");
// → { valid: false, reason: "Failed Luhn checksum" }What the Luhn Algorithm Does Not Do
It's worth being clear on the limits:
| What it checks | What it doesn't check |
|---|---|
| ✅ Structural plausibility | ❌ Whether the card account exists |
| ✅ Catches most single-digit typos | ❌ Whether the card has funds |
| ✅ Catches most transpositions | ❌ Whether the card is expired |
| ✅ Runs entirely client-side | ❌ Whether the CVC is correct |
The Luhn check is a first filter - a fast, zero-cost sanity check that runs before any network request is made. The actual authorization happens server-side through your payment gateway (Stripe, Razorpay, etc.), which talks directly to the card networks.
Why This Matters as a Developer
Understanding this gives you three practical wins:
-
Better UX. You can show real-time validation feedback as the user types - network logo, length indicator, and checksum - without hitting an API.
-
Fewer failed API calls. Catching invalid numbers client-side means fewer wasted payment processor API calls, which can add up at scale.
-
Security hygiene. Never log or store raw card numbers on your servers. Even in test environments. PCI DSS compliance starts with understanding what card data is.
Try It: Live Luhn Checker
If you want to play with this yourself, paste the Luhn function into your browser console and test it with Stripe's official test card numbers:
luhn("4242 4242 4242 4242"); // Stripe test Visa → true
luhn("5555 5555 5555 4444"); // Stripe test Mastercard → true
luhn("3782 822463 10005"); // Stripe test Amex → trueSummary
Apps validate card numbers in three fast, client-side steps:
- BIN check - identify the card network from the first 6-8 digits
- Length check - verify the number of digits matches the network
- Luhn algorithm - run the mod-10 checksum to catch typos
The Luhn algorithm is elegant precisely because it's so simple: just doubles, subtracts, and sums. Yet it catches the vast majority of accidental errors in under a millisecond, without ever touching a server.
Next time a form catches your typo before you hit submit, you'll know exactly what ran under the hood.