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.
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:
| Check | What it means | What it does not mean |
|---|---|---|
| Luhn check digit | The last digit is mathematically consistent with the rest | The number belongs to a real card |
| Network prefix | The number starts within a published IIN range | The prefix is assigned to an active issuer |
| Length | The digit count matches that network’s rules | The card was ever issued |
| Character set | Only digits, in a valid count | Anything 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 does | Checks format and the Luhn checksum | Tests whether a card is active |
| How | Arithmetic, offline, in your browser | Sends the card to a payment network |
| Legitimate | Yes — every payment form does this | No |
| Whose card | Test data you typed yourself | Usually 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:
- 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.
- 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.
- 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.
- 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.
- 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.