The Luhn Algorithm for Credit Cards

How the Luhn checksum protects credit-card numbers, where it came from, a TypeScript implementation, and why it does not prove the card is valid.

Every credit-card number you have ever typed into a checkout form ends in a digit that exists for one reason: to catch typos. That digit is computed with the Luhn algorithm— a deceptively simple weighted-sum checksum invented in the 1950s and now embedded in payment networks, gift cards, IMEI numbers and several national identity schemes. This article explains how Luhn works, where it came from, how to implement it correctly, and what it does not tell you about a card.

A short history

Hans Peter Luhn, a German-born researcher at IBM, filed U.S. Patent 2,950,048in 1954, granted in 1960, for a “Computer for Verifying Numbers”. The patent expired decades ago, which is why the algorithm now appears unencumbered in ANSI X4.13, ISO/IEC 7812, RFC 7468 and most credit-card software in existence. Luhn's design constraint was magstripe verification on 1960s hardware: the algorithm had to fit in a few hundred transistors and finish before the card finished sliding through the reader. He landed on a weighted sum modulo 10, which uses only single-digit additions and one final modulo. The math is elementary; the wider impact is that Luhn is the reason most card-payment systems detect 90%+ of single-character data-entry errors before they reach the bank.

The algorithm

Take any decimal number. Starting from the rightmost digit and moving left, double every second digit. If doubling produces a two-digit result (10 to 18), add the two digits together (or, equivalently, subtract 9). Sum all the resulting digits. The number is Luhn-valid iff the sum is divisible by 10.

Worked example for 4539 1488 0343 6467:

Position (from right):  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 every 2nd?         x     x     x     x     x     x     x     x
After doubling:           8  5  6  9  2  4 16  8  0  3  8  3 12  4 12  7
After digit-sum reduce:   8  5  6  9  2  4  7  8  0  3  8  3  3  4  3  7

Total = 80, 80 % 10 == 0, so the number is Luhn-valid.

A TypeScript implementation

function isLuhnValid(pan: string): boolean {
  const digits = pan.replace(/\D/g, '');
  if (digits.length === 0) return false;

  let sum = 0;
  let shouldDouble = false; // doubling pattern starts at the rightmost digit

  for (let i = digits.length - 1; i >= 0; i--) {
    let d = digits.charCodeAt(i) - 48;
    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
    shouldDouble = !shouldDouble;
  }

  return sum % 10 === 0;
}

Notes on this implementation:

  • replace(/\D/g, '') strips spaces and dashes — users routinely paste numbers in4242 4242 4242 4242 form.
  • .charCodeAt(i) - 48 avoids the cost of parseInt in tight loops.
  • d > 9 ? d - 9 : d is mathematically identical to summing the two digits of d.

To generatea Luhn-valid number, append a placeholder 0, run the validator on it, and the “needed digit” is (10 - sum % 10) % 10. Replace the placeholder with that digit. The same arithmetic that checks numbers also produces them.

Card networks and IIN ranges

Card numbers are governed by ISO/IEC 7812. The first 6 digits (or 8 since 2022) are the Issuer Identification Number (IIN), assigned by the network. The IIN tells you the network and (often) the issuer:

  • Visa — starts with 4, length 13, 16 or 19.
  • Mastercard — starts with 51–55 or 2221–2720, length 16.
  • American Express — starts with 34 or 37, length 15.
  • Discover — starts with 6011, 644–649 or 65, length 16.
  • JCB — starts with 3528–3589, length 16.
  • Diners Club — starts with 300–305, 36 or 38, length 14.
  • UnionPay — starts with 62, length 16–19.

All seven of those networks compute the final digit with Luhn. So do most gift cards, IMEI numbers (mobile-device identifiers), Canadian SINs and Israeli ID numbers. The only widely used payment-related identifier that does not use Luhn is the IBAN; it uses modulo 97 instead.

Why Luhn does not prove the card is real

Luhn proves three things:

  1. The number was not corrupted by a single-digit typo.
  2. Adjacent digits were not transposed (catches the most common typo class).
  3. The number is well-formed enough for the network to route an authorization request.

Luhn does not prove:

  • That the number was ever issued by a real bank.
  • That the card has not expired.
  • That the cardholder authorised this particular charge.
  • That funds are available.

For all of those, you need a real authorization round-trip. That is why every fraud-prevention manual recommends rejecting Luhn-invalid numbers silentlyand rate-limiting attempts — an attacker who can probe Luhn-valid numbers freely could walk a BIN/IIN range.

Test card numbers per issuer

For sandbox testing with real payment service providers, use the numbers theypublish, not arbitrary Luhn-valid numbers, because the PSP's sandbox keys those numbers to specific response codes. Quick reference:

  • Stripe: 4242 4242 4242 4242 (Visa, success), 5555 5555 5555 4444 (Mastercard), 3782 822463 10005 (Amex). Stripe maintains the canonical list at stripe.com/docs/testing.
  • Adyen: published per-method at docs.adyen.com with explicit success / decline / 3DS-challenge variants.
  • Mollie: any Luhn-valid number on the test API succeeds; specific failures are triggered with thetestmode_status query parameter.
  • PayPal Sandbox: uses Sandbox-issued cards on test buyer accounts, not raw PANs.

For unit and integration testing of your own validators (not against a real PSP), Luhn-valid synthetic numbers from the credit-card generator are the safer choice. They look real, they pass Luhn, and they carry no risk of accidentally hitting a real card.

Common pitfalls

  • Reverse-direction confusion.The doubling pattern starts at the rightmost digit, not the leftmost. Implementations that loop forward and toggle “double” based oni % 2 silently produce wrong results for odd-length numbers like Amex (15 digits).
  • Logging the full PAN. Even a Luhn-valid synthetic number stored in production logs makes that log a PCI-DSS-relevant artifact for the auditor. Mask all but the last 4 digits before logging anything.
  • Using Luhn-valid numbers as IDs.Some teams use random Luhn-valid 16-digit strings as internal account identifiers, then later wonder why their fraud-detection system flags them. Use UUIDs for internal IDs — the UUID generator can mint those.
  • Believing Luhn provides any security. Luhn is a checksum, not a hash. An attacker who knows the algorithm can produce arbitrary valid numbers in microseconds. Anti-fraud systems must rely on issuer authorization, not Luhn.

Frequently asked questions

Does a Luhn-valid number mean the card is real?

No. Luhn is a checksum that detects typos. Whether the card actually exists, has funds, or belongs to the cardholder requires a real authorization request to the issuing bank. Luhn is the first cheap filter; the issuer's online check is the truth.

Why do Visa, Mastercard, Amex and Discover all use Luhn?

When the major card networks standardised on ISO/IEC 7812 in the 1980s, Luhn was already the de facto checksum for IBM punched-card systems. ISO/IEC 7812 enshrines Luhn as the check-digit algorithm for the entire Primary Account Number (PAN), so every modern PAN-based card — Visa, Mastercard, American Express, Discover, JCB, UnionPay, Diners Club — uses Luhn for the final digit.

What is the difference between Luhn and IBAN MOD-97?

Luhn operates on a single decimal number and uses a weighted sum modulo 10. IBAN MOD-97 operates on a string that may contain letters (which are first converted to two-digit numbers) and uses a much larger modulus. MOD-97 catches more error patterns but is slower; Luhn is fast enough to run in real-time on a magstripe terminal, which was the original design constraint.

What are the official Stripe / Adyen test card numbers?

Stripe's most-used test number is 4242 4242 4242 4242 (Visa, succeeds). 4000 0000 0000 0002 simulates a generic decline; 4000 0000 0000 9995 simulates insufficient funds. Adyen exposes a similar set documented at docs.adyen.com/development-resources/testing/test-card-numbers. Always use the test numbers your PSP publishes for sandbox testing — they trigger predictable response codes that our generic Luhn-valid numbers do not.

Try it yourself. Generate Luhn-valid synthetic credit-card numbers per network with the credit-card generator, or read the related deep-dive on IBAN MOD-97 validation.