Skip to content
Innovialabs

Luhn Algorithm: Validating Uzcard, Humo and Visa Card Numbers

Practice5 min

In short

The Luhn algorithm is a simple checksum that catches most typos in a card number before the form is submitted; it cannot tell whether a card exists — only the payment provider can.

What the Luhn algorithm checks

The Luhn algorithm (mod 10) is a checksum formula. The last digit of a card number is chosen so that a sum calculated by a specific rule is divisible by 10. If a user mistypes one digit or swaps two neighbouring digits, the sum almost always stops adding up.

What Luhn does not do:

  • check that the card exists and is active;
  • check the balance, expiry date or owner;
  • protect against fraud.

It is a user-experience check: flag a typo immediately instead of after a round trip to the provider.

How the sum is calculated

  1. Walk the digits from right to left. The last digit is the check digit; leave it as is.
  2. Multiply every second digit (second from the right, fourth from the right and so on) by 2.
  3. If the result is greater than 9, subtract 9 (the same as adding its digits: 16 → 1 + 6 = 7).
  4. Add up all the digits.
  5. The number is valid if the sum is divisible by 10.

Example with 79927398713: right to left we get 3, 2, 7, 7, 9, 6, 7, 4, 9, 9, 7. The sum is 70, divisible by 10, so the number passes.

A known limitation: swapping adjacent 09 ↔ 90 goes unnoticed.

JavaScript implementation

function luhnValid(input) {
  const digits = String(input).replace(/[\s-]/g, "");
  if (!/^[0-9]{12,19}$/.test(digits)) return false;

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

luhnValid("4111 1111 1111 1111"); // true
luhnValid("4111 1111 1111 1112"); // false

Python implementation

import re

def luhn_valid(number: str) -> bool:
    digits = re.sub(r"[\s-]", "", number)
    if not re.fullmatch(r"[0-9]{12,19}", digits):
        return False
    total = 0
    for i, ch in enumerate(reversed(digits)):
        d = int(ch)
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

assert luhn_valid("4111 1111 1111 1111")
assert not luhn_valid("4111-1111-1111-1112")

Note that digits are matched with [0-9], not \d. In Python, \d also matches digits from other scripts by default.

Card type by BIN

The first digits of the number — the BIN (IIN) — identify the payment network and the issuing bank. For an interface hint (a logo, the mask length) a prefix is usually enough:

NetworkTypical prefix
Humo9860
Uzcard8600
Visa4
Mastercard51–55, 2221–2720
function detectBrand(digits) {
  if (digits.startsWith("9860")) return "humo";
  if (digits.startsWith("8600")) return "uzcard";
  if (digits.startsWith("4")) return "visa";
  const p2 = Number(digits.slice(0, 2));
  const p4 = Number(digits.slice(0, 4));
  if ((p2 >= 51 && p2 <= 55) || (p4 >= 2221 && p4 <= 2720)) return "mastercard";
  return "unknown";
}

Keep in mind:

  • BIN ranges change; new ranges and co-badged cards appear. Keep the prefix table in config rather than scattered through the code.
  • Check longer prefixes before shorter ones.
  • For local networks, confirm in your provider's documentation whether the Luhn check applies to their numbers. If you are not sure, show a warning rather than blocking submission.
  • unknown is not an error. Do not stop a user from paying with a card your code does not recognise.

Input mask

Grouping by 4 digits makes the number much easier to check by eye:

const format = (v) =>
  v.replace(/[^0-9]/g, "").slice(0, 19).replace(/(\d{4})(?=\d)/g, "$1 ");

Practical tips:

  • Use <input inputmode="numeric" autocomplete="cc-number">, not type="number": the latter mangles leading zeros and shows spinner arrows.
  • Accept pasted values with spaces and dashes — clean them instead of rejecting them.
  • When reformatting, preserve the caret position, or it jumps to the end when the user edits in the middle.
  • Run the Luhn check once the full length is entered, not on every keystroke.

Real validation happens at the provider

Client-side checks only filter typos. Whether the card exists, the balance, 3-D Secure or SMS confirmation and limits are checked by the payment provider.

  • Where possible, use the provider's payment page or hosted fields and tokenization, so the card number never passes through your server.
  • Do not store the full card number in your database or logs. Handling card data is governed by the PCI DSS standard.
  • Repeat the check on the server if the number does reach you: client code is easy to bypass.

FAQ

No. Luhn only confirms that the digits are consistent with each other. Generating a number with a valid checksum is trivial. Only the provider can confirm a card exists, when it authorizes a payment.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.