Paste a card number and this tool tells you whether it is correctly formatted: whether the Luhn check digit is right, which network the prefix belongs to, and whether the length matches that network’s rules. Everything runs in your browser.

It does not tell you whether a card is real, active, or has funds. Nothing on the open internet can tell you that, and a tool that claimed to would be doing something illegal. We explain the difference below.

Runs in your browser

Card Number Validator

Checks the Luhn check digit, the network prefix and the length. The number is never sent anywhere and is not stored.

Format validation only. A well-formed number is not a real card, and nothing here can tell you whether a card exists, is active, or has funds.

A note if you are validating a real card number. This page runs entirely in your browser and the number you paste is never transmitted or stored. That said, browser extensions can read page content, so close the tab afterwards. For real card data, your bank’s own tools are the safer place.

What this validator checks

Four checks, and it is worth being explicit about the gap between what each one proves and what people assume it proves:

CheckWhat it meansWhat it does not mean
Luhn check digitThe last digit is mathematically consistent with the restThe number belongs to a real card
Network prefixThe number starts within a published IIN rangeThe prefix is assigned to an active issuer
LengthThe digit count matches that network’s rulesThe card was ever issued
Character setOnly digits, in a valid countAnything at all about the account

The right-hand column is the whole story. Every check here is a statement about the digits. None of them reaches outside the page, because there is nothing outside the page to reach.

Validator vs checker: why we only do one

The two words get used interchangeably, and they describe completely different things:

Validator (this page)“Checker”
What it doesChecks format and the Luhn checksumTests whether a card is active
HowArithmetic, offline, in your browserSends the card to a payment network
LegitimateYes — every payment form does thisNo
Whose cardTest data you typed yourselfUsually a stolen card list

A validator answers a question about the number itself: is it well-formed? That is a pure arithmetic question, and it is exactly what every payment form runs before it submits anything — it saves the user a round-trip when they have simply mistyped a digit.

A “checker” answers a question about someone’s account: is this card active? Answering that requires sending the card to a payment network, which means either attempting a small transaction or abusing an authorisation endpoint. When it is your own card, your bank’s app already tells you. When it is not your own card, running that check is unauthorised access, and the tools that do it exist to sort stolen card lists.

We build the first. We will not build the second.

The Luhn algorithm

The rule is short enough to state completely. Walking right to left, double every second digit; if doubling takes a digit above nine, subtract nine. Sum everything. A valid number totals a multiple of ten.

function validateLuhn(pan) {
  const digits = pan.replace(/\D/g, '');
  if (digits.length < 12 || digits.length > 19) {
    return { valid: false, reason: 'length out of range (12-19)' };
  }

  let sum = 0;
  let double = false;

  for (let i = digits.length - 1; i >= 0; i--) {
    let d = Number(digits[i]);
    if (double) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
    double = !double;
  }

  return { valid: sum % 10 === 0, checksum: sum };
}

Note that doubling starts at false, because the rightmost digit is the check digit and is never doubled. Getting that flag backwards is the classic Luhn bug, and it produces a function that passes exactly the numbers it should reject.

To tell a user what went wrong rather than just that something did, compute the digit the checksum expected. Here the flag starts at true, because the position the check digit will occupy is not part of the input:

function expectedCheckDigit(panWithoutCheck) {
  let sum = 0;
  let double = true;
  for (let i = panWithoutCheck.length - 1; i >= 0; i--) {
    let d = Number(panWithoutCheck[i]);
    if (double) { d *= 2; if (d > 9) d -= 9; }
    sum += d;
    double = !double;
  }
  return (10 - (sum % 10)) % 10;
}

That second function is what turns “invalid card number” into “the last digit should be 4” — and it is also how every number from the all-network generator is completed.

One property worth knowing before you rely on it: Luhn catches every single-digit error and almost every transposition of adjacent digits, but it misses transposing 09 and 90. It is a typo filter with known gaps, not a proof of anything.

Implementations in other languages

Python:

def luhn_valid(pan: str) -> bool:
    digits = [int(c) for c in pan if c.isdigit()]
    checksum = 0
    for i, d in enumerate(reversed(digits)):
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        checksum += d
    return checksum % 10 == 0

PHP:

function luhn_valid(string $pan): bool
{
    $digits = preg_replace('/\D/', '', $pan);
    $sum = 0;
    $double = false;

    for ($i = strlen($digits) - 1; $i >= 0; $i--) {
        $d = (int) $digits[$i];
        if ($double) {
            $d *= 2;
            if ($d > 9) {
                $d -= 9;
            }
        }
        $sum += $d;
        $double = !$double;
    }

    return $sum % 10 === 0;
}

The shape is identical in every language because the algorithm has no room for interpretation: iterate from the right, alternate the doubling, sum, take the total modulo ten. Java, C#, Go and Ruby versions follow the same structure, and the algorithm guide works through why it behaves the way it does.

Where Luhn validation belongs in your form

Five pieces of advice that between them cover most of what goes wrong:

  1. Run it on blur, or when the expected length is reached — not on every keystroke. A number is invalid for almost its entire typing lifetime. Showing an error at digit four trains people to ignore your errors.
  2. Write the message for a person, not a specification. “Please check your card number” is right. “Invalid Luhn checksum” tells the user nothing they can act on, and it leaks implementation detail into your UI.
  3. Warn, do not block. Let the form submit anyway. Ranges change, new BINs appear, and the occasional card really does behave unexpectedly — a hard client-side block converts every one of those into an abandoned checkout for no gain.
  4. Validate server-side as well. Anything enforced only in the browser is not enforced. The client check is for the user’s benefit; the server check is for yours.
  5. Never treat it as fraud detection. Luhn catches typing errors. It says nothing about whether the card exists, who holds it, or whether the transaction is legitimate. That judgement belongs to your processor’s risk engine.

The same reasoning applies to an unrecognised prefix, which is why this validator reports it as unknown rather than invalid. New BIN ranges are assigned continuously and any list of prefixes starts going stale the day it is written — Mastercard’s 2-series is the standing example, still rejected by validation written against ^5[1-5] years after those cards entered circulation. Detect the brand when you can, fall back gracefully when you cannot, and let the authorisation decide.

For which lengths and prefixes to accept per network, the Visa, Mastercard and American Express pages carry the format tables — Visa alone permits 13, 16 and 19 digits, which is the length rule most often written too narrowly.

Testing your validation

Two sources of input, for two different jobs. Generate synthetic numbers here or with the BIN generator to exercise your format rules, including deliberately broken cases: change one digit of a valid number and confirm your code rejects it and reports the expected digit. When you need the processor to respond rather than your own code, switch to the test card numbers reference — Stripe publishes a number that fails Luhn on purpose, 4242 4242 4242 4241, precisely so you can test the branch where your own validation should have caught the input first.

The tool directory lists everything else, and the FAQ covers what a Luhn-valid number does and does not prove.

Frequently Asked Questions

That the final digit of a card number is arithmetically consistent with the digits before it. Every other digit is doubled from the right, digits over nine have nine subtracted, everything is added up, and the total must be a multiple of ten. It is a checksum designed in the 1950s to catch mistyped and transposed digits, and that is the whole of what it does.
No. Luhn validity is a property of the digits, not evidence of an account. Any number can be made Luhn-valid by choosing the right last digit, which is exactly how every test number on this site is produced. A real card is Luhn-valid; a Luhn-valid number is very probably not a real card.
No, and it never will. Answering that requires sending the number to a payment network, which means either attempting a transaction or abusing an authorisation endpoint. For your own card, your bank’s app already tells you. For anyone else’s, it is unauthorised access. Tools that offer this exist to sort stolen card lists, and we are not building one.
No. The validation is arithmetic performed by JavaScript in your browser, and there is no network request involved. You do not have to take our word for it: open your browser’s developer tools, switch to the Network tab, and type a number into the field. Nothing is sent. The value is also not written to local storage, so a reload discards it.
Almost always a mistyped or transposed digit — that is precisely the error Luhn was designed to catch. When the check fails we show which digit the checksum expected, so comparing it against the card usually locates the mistake immediately. If the number is definitely correct, check that you have not dropped a leading digit when copying.
Effectively all of them today. The check digit is part of ISO/IEC 7812, and Visa, Mastercard, American Express, Discover, JCB and UnionPay all issue Luhn-valid numbers. UnionPay is the interesting footnote: some cards issued in the mid-2010s did not carry a valid check digit, which is one reason a failed Luhn check should warn rather than block.
Yes, as a courtesy to the user, and never as a gate. Running it client-side saves someone a failed authorisation when they have simply mistyped a digit. Treating it as authoritative is the mistake: it tells you nothing about whether a card exists, and a hard block turns any edge case into a lost sale. Warn, allow submission, and let the processor make the real decision.