Luhn Check in JavaScript: Validate Card Numbers and IMEIs
The last digit of every payment card number and every 15-digit IMEI is a Luhn checksum. Here is a compact, tested JavaScript function to verify it — and the mistakes that make most copy-pasted versions wrong.
What the Luhn algorithm is
The Luhn algorithm (ISO/IEC 7812) is the mod-10 checksum used on payment card numbers, 15-digit IMEIs, and — in an expanded form — ISIN securities identifiers. Its job is to catch typos: mistype one digit anywhere in a card number and the check fails, guaranteed. If you want the general theory of how these schemes trade off error detection, see how check digits work; if you just want to test a number right now, the Luhn checker does it in the browser.
The rule is short enough to state in three lines:
- Walk the digits from right to left. The rightmost digit (the check digit itself) is not doubled; then double every second digit.
- Fold any doubled value above 9 back to a single digit by subtracting 9 (16 → 7, which equals 1 + 6).
- Sum everything. The number is valid if the total is divisible by 10.
A tested implementation
This function accepts a string (spaces and dashes allowed, since users paste card numbers as 4539 1488 0343 6467), rejects anything else, and returns a boolean. It was run against the test vectors listed below before publishing.
function luhnCheck(value) {
const digits = String(value).replace(/[\s-]/g, ''); // strip spaces and dashes
if (!/^\d+$/.test(digits) || digits.length < 2) return false;
let sum = 0;
let shouldDouble = false; // rightmost digit is NOT doubled
for (let i = digits.length - 1; i >= 0; i--) {
let d = digits.charCodeAt(i) - 48; // char to digit
if (shouldDouble) {
d *= 2;
if (d > 9) d -= 9; // fold: 16 -> 1 + 6 = 7
}
sum += d;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
luhnCheck('4539 1488 0343 6467'); // true (Visa test number)
luhnCheck('490154203237518'); // true (IMEI)
luhnCheck('79927398710'); // false (last digit should be 3)
Line by line: the replace removes only whitespace and dashes — deliberately not "all non-digits", so that garbage like 4539x1488... is rejected by the regex on the next line instead of being silently "cleaned" into a valid number. The loop runs right to left with a boolean that flips each step, which handles odd and even lengths correctly without any position arithmetic. charCodeAt(i) - 48 converts a character to its digit value without allocating substrings.
Worked example: 79927398713
The classic Luhn test vector. Doubled positions counted from the right are the 2nd, 4th, 6th, 8th and 10th:
| Digit | 7 | 9 | 9 | 2 | 7 | 3 | 9 | 8 | 7 | 1 | 3 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Doubled? | – | ×2 | – | ×2 | – | ×2 | – | ×2 | – | ×2 | – |
| Contributes | 7 | 18→9 | 9 | 4 | 7 | 6 | 9 | 16→7 | 7 | 2 | 3 |
Sum: 7 + 9 + 9 + 4 + 7 + 6 + 9 + 7 + 7 + 2 + 3 = 70. 70 mod 10 = 0, so 79927398713 is valid — and changing the final 3 to a 0 makes the sum 67 and the check fails, exactly as the sample output above shows.
Generating a check digit is the same loop with the parity shifted, because every position moves one step left once the check digit is appended:
function luhnDigit(body) { // body = number WITHOUT its check digit
const digits = String(body).replace(/[\s-]/g, '');
if (!/^\d+$/.test(digits)) throw new TypeError('digits only');
let sum = 0;
let shouldDouble = true; // parity shifts by one
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 (10 - (sum % 10)) % 10;
}
luhnDigit('7992739871'); // 3
luhnDigit('49015420323751'); // 8 -> IMEI 490154203237518
Edge cases and classic mistakes
- Keep numbers as strings. A 16-digit value is near JavaScript's
Number.MAX_SAFE_INTEGER(253), and 17–19 digit card numbers exceed it:9999999999999999 === 10000000000000000evaluates totrue. Converting to a number also destroys leading zeros — the same bug that mangles barcodes in Excel. NeverparseIntan identifier. - Wrong parity. The most common bug in copy-pasted Luhn code is doubling from the left, or doubling the rightmost digit. The alternation must be anchored at the right end: check digit not doubled, its neighbor doubled. Left-anchored code passes even-length tests and silently fails odd-length inputs like 15-digit Amex numbers and IMEIs.
- Forgetting the fold. Summing 16 as 16 instead of 7 produces a checksum that accepts and rejects the wrong numbers. If a doubled value exceeds 9, subtract 9 (equivalent to adding its two digits).
- Luhn does not check length.
luhnCheck('059')returnstrue— correctly, because the math holds. If you are validating a specific identifier, add its length rule: 15 digits exactly for an IMEI, 13–19 for payment cards (with 16 dominant), plus an IIN prefix check if you need to detect the brand. - Clean, don't repair. Strip separators the user legitimately types (spaces, dashes) and reject everything else. Silently deleting letters can turn an obviously corrupted paste into a "valid" number.
Your barcode check digit is not Luhn
Search results routinely mix these up, and the confusion runs in both directions. The check digit on UPC, EAN, GTIN-14 and SSCC barcodes uses the GS1 mod-10 algorithm: alternate digits are multiplied by 3 and 1, and the products are not folded. Same modulus, different weighting, incompatible results. The valid UPC-A 036000291452 fails luhnCheck (we verified this while testing), and a valid Visa number will not generally satisfy GS1 mod-10. If you need barcode validation, use the GTIN check digit calculator or the GS1 walkthrough in how check digits work — not the code on this page.
Checksum, not verification — and never log real cards
A passing Luhn check means the number is self-consistent, nothing more. About 10% of random digit strings pass by construction. It does not tell you the card was issued, is active, or belongs to your customer — only the issuing bank knows that, via your payment processor. Treat Luhn as a front-end typo filter that saves a round trip, never as fraud screening.
Two practical rules follow. First, develop and test with designated test numbers (4111 1111 1111 1111, 4539 1488 0343 6467) — never with a real card. Second, real card numbers (PANs) must not end up in your logs, analytics events, error reports, or URL parameters; PCI DSS forbids storing them unmasked, and a stray console.log(cardNumber) shipped to a browser-monitoring service is exactly how that happens. Validate in memory, pass the value straight to your payment provider's tokenizer, and log at most the last four digits.
Validating at scale?
If you are checking card-shaped identifiers or IMEIs in bulk — deduplicating a device inventory, cleaning a CSV import — the CodeClassify API runs the same validation server-side, up to 100 numbers per call:
curl -X POST https://codeclassify-api.rosariovitale0096.workers.dev/v1/luhn/validate \
-H "X-Api-Key: ccl_your_key" \
-H "Content-Type: application/json" \
-d '{"numbers":["4539148803436467","79927398710"]}'
{"ok":true,"count":2,"valid":1,"results":[
{"input":"4539148803436467","digits":"4539148803436467","valid":true},
{"input":"79927398710","digits":"79927398710","valid":false,"expected_check_digit":3}
]}
Invalid entries include the expected check digit so you can flag likely typos. The free tier includes 10 calls per month with no card required — details and signup at the API page, and there is a guide to bulk-validating a CSV end to end.
FAQ
Does passing the Luhn check mean a credit card number is real?
No. The Luhn check only confirms the number is internally consistent — that its last digit matches the checksum of the others. It does not prove the card was ever issued, is active, or has funds. Roughly one in ten random digit strings passes Luhn by chance. Real verification happens when the payment processor contacts the issuing bank; use the Luhn check only as a cheap typo filter before that.
Is the Luhn algorithm the same as the barcode (GS1) check digit?
No, and this is a common source of bugs. Both are mod-10 schemes, but Luhn doubles alternate digits and folds two-digit results back to one digit (16 becomes 7), while the GS1 algorithm used on UPC, EAN and other GTIN barcodes multiplies alternate digits by 3 and keeps the products whole. A valid GTIN such as 036000291452 fails the Luhn test, and a valid card number will not generally pass GS1 mod-10.
Can the Luhn algorithm validate IMEI numbers too?
Yes. A 15-digit IMEI ends in a Luhn check digit computed over the first 14 digits, so the same function that validates card numbers validates IMEIs — just add a length check for exactly 15 digits. Note that the 16-digit IMEISV variant replaces the check digit with a two-digit software version and has no checksum at all, so a Luhn test on an IMEISV is meaningless.
Test a number without writing code
Paste any card-shaped number or IMEI into the Luhn checker to validate it instantly in your browser — nothing you type is sent to a server.
This guide is for general information only. A Luhn checksum confirms internal consistency of a number, not that a card or device identifier is issued, active, or legitimate. Never collect, store, or log real payment card numbers outside a PCI-compliant flow; use your payment provider's hosted fields or tokenizer for anything touching real cards. See our methodology for how validations are implemented.