Test Card Numbers and the Luhn Algorithm, Explained
Every payment form you have ever built or tested runs the same silent check before it talks to a payment API: the Luhn algorithm. It is a one-digit checksum from 1954 that catches most typos in a card number before the request ever leaves the browser. Understanding it explains why test data generators can produce card numbers that pass form validation, and why those numbers are still completely inert — they cannot be charged, hold no funds, and are not connected to any issuing bank.
This guide walks through the anatomy of a card number, the Luhn checksum step by step, what the check does and does not verify, and the right way to divide testing work between generated test card numbers and the official sandbox cards published by payment providers such as Stripe, PayPal, and Adyen.
The anatomy of a card number
A payment card number is not a random string of digits. The first six to eight digits are the Issuer Identification Number (IIN), often called the BIN, and they identify the card network and issuing institution. Numbers starting with 4 belong to Visa, 51 through 55 (and the newer 2221–2720 range) to Mastercard, and 34 or 37 to American Express. This prefix is why a checkout form can show the right network logo the moment you type the first couple of digits.
Length varies by network: Visa and Mastercard numbers are typically 16 digits, while Amex uses 15. The digits between the IIN and the end identify the individual account, and the very last digit is special — it is the check digit, computed from all the preceding digits using the Luhn algorithm. Change any single digit in the number and the check digit no longer matches, which is exactly the point.
How the Luhn checksum works, step by step
The algorithm is simple enough to verify by hand. Starting from the rightmost digit (the check digit) and moving left, double every second digit. If doubling produces a two-digit result, add its digits together — so 16 becomes 7 and 18 becomes 9. Then sum all the digits, doubled and undoubled alike. If the total is a multiple of 10, the number passes.
A worked example makes it concrete. Take the payload 7992739871 and compute its check digit. Walking right to left, the digits in the doubling positions are 1, 8, 3, 2, and 9; doubled they become 2, 16, 6, 4, and 18, which reduce to 2, 7, 6, 4, and 9 — a subtotal of 28. The untouched digits 7, 9, 7, 9, and 7 add up to 39, for a running total of 67. The check digit is whatever brings that total to the next multiple of 10: here, 3. The full Luhn-valid number is therefore 79927398713, and any transcription error in it — a mistyped digit, or most swaps of two adjacent digits — will break the checksum.
Why payment forms validate with Luhn on the client
The Luhn check exists to catch honest mistakes early. When someone types a card number by hand, single-digit typos and adjacent-digit transpositions are by far the most common errors, and Luhn detects nearly all of them instantly, in the browser, without a network round trip. That means faster feedback for the user, fewer doomed requests hitting the payment API, and fewer confusing decline messages for what is really just a typo.
For developers and QA this client-side layer is a testing surface of its own: the input mask, the network detection from the IIN prefix, the length rules per network, and the Luhn validation all need test input that looks structurally right. That is the job generated test card numbers are made for.
What Luhn does not verify
It is worth being blunt about the limits. Luhn is a format check, nothing more. A number that passes it says only that the digits are internally consistent. It does not mean an account with that number exists, that any bank issued it, that there are funds behind it, or that a charge could ever succeed. The checksum also has no connection to the CVV, the expiry date, the cardholder name, or the billing address — those are verified by the issuing bank during authorization, a process a generated number never reaches.
This is why Luhn-valid numbers from a test data generator are safe by construction for their intended use: they satisfy the arithmetic that a form checks, and absolutely nothing beyond it. There is no account, no balance, and no issuer on the other side.
How generators produce Luhn-valid test numbers
A test data generator simply runs the algorithm in reverse. It picks a network prefix — say 4 for a Visa-style number — fills the middle positions with random digits up to one short of the target length, computes the Luhn sum of that payload, and appends the one check digit that makes the total divisible by 10. The result is structurally indistinguishable from a real card number as far as a form’s validation logic is concerned, which is precisely what UI testing needs.
Because the middle digits are random, such a number is not tied to any account. It will pass an input mask and a client-side Luhn check, and it will be declined the moment anything tries to authorize it, because no issuing bank recognizes it and there is nothing to charge.
Sandbox cards, expiry dates, and CVV in test data
Draw a clear line between two testing jobs. For input masks, formatting, network-logo detection, and client-side validation, generated Luhn-valid numbers are the right tool. For anything that touches a payment API — tokenization, authorization, 3-D Secure flows, declines, refunds, webhooks — use the official test card numbers your provider publishes. Stripe, PayPal, and Adyen each maintain documented lists of sandbox cards designed to trigger specific outcomes, such as a successful charge, an insufficient-funds decline, or an authentication challenge. Those numbers only work in sandbox mode and are the only legitimate way to exercise a payment integration end to end.
Round out card test data with plausible companion fields. Expiry dates should be in the future for the happy path, with at least one past date to test your expiry validation; CVV values are three random digits (four for Amex-style numbers). Like the card number itself, generated expiry and CVV values verify nothing real — they exist so your form logic, error states, and storage rules can be tested without any real payment credentials in your fixtures.
Frequently Asked Questions
Can a generated Luhn-valid number be charged? +
No. Passing the Luhn check only means the digits are arithmetically consistent. A generated number has no issuing bank, no account, and no funds behind it, so any authorization attempt fails immediately. It is useful strictly for testing form validation and UI behavior.
Why does my payment provider still decline a Luhn-valid number? +
Because Luhn is only the first, client-side gate. Real authorization asks the card network and issuing bank whether the account exists, is active, and has funds — and a generated number fails at that step by design. For API-level tests, use the official sandbox test card numbers from your provider’s documentation.
Which test numbers should I use for Stripe, PayPal, or Adyen integration tests? +
Use the test card lists those providers publish in their developer docs. Each sandbox number is wired to a specific scenario — success, decline, 3-D Secure challenge — so your integration tests are deterministic. Generated numbers belong in front-end and fixture testing, not in payment API calls.
Does the Luhn algorithm validate the CVV or expiry date? +
No. The checksum covers only the card number’s own digits. CVV and expiry are independent fields checked by the issuer during authorization, so your form needs separate validation for them: a future expiry date and a three-digit CVV (four for Amex-format numbers).