Back to writing

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)
PartDigitsWhat it means
IIN / BINFirst 6–8 digitsIdentifies the card network and issuing bank
Account numberMiddle digitsUnique to your account
Check digitLast digitUsed purely for validation

For example:

  • 4 → Visa
  • 51–55 or 2221–2720 → Mastercard
  • 34 or 37 → American Express
  • 6011 → 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:

NetworkLength
Visa13 or 16 digits
Mastercard16 digits
Amex15 digits
Discover16 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

  1. Starting from the rightmost digit (the check digit), move left.
  2. Double every second digit (i.e., every digit at an even position from the right).
  3. If doubling produces a number > 9, subtract 9.
  4. Sum all digits.
  5. 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:     ✓    ✓     ✓     ✓    ✓     ✓     ✓     ✓
DigitActionResult
4×2 = 88
5keep5
3×2 = 66
9keep9
1×2 = 22
4keep4
8×2 = 16 → 16-97
8keep8
0×2 = 00
3keep3
4×2 = 88
3keep3
6×2 = 12 → 12-93
4keep4
6×2 = 12 → 12-93
7keep7

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 checksWhat 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:

  1. Better UX. You can show real-time validation feedback as the user types - network logo, length indicator, and checksum - without hitting an API.

  2. Fewer failed API calls. Catching invalid numbers client-side means fewer wasted payment processor API calls, which can add up at scale.

  3. 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 → true

Summary

Apps validate card numbers in three fast, client-side steps:

  1. BIN check - identify the card network from the first 6-8 digits
  2. Length check - verify the number of digits matches the network
  3. 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.