What problem does a check digit solve?
You’re typing a 12-digit Aadhaar into a KYC form. You hit the wrong key on digit 7. Without a check digit, the form has no way to know — it accepts your number and ships it to the backend, where some downstream system either rejects it three steps later, or worse, accepts it and matches the wrong person.
A check digit is a single extra digit appended to a number so that the number “checks itself”. Change any digit and the math no longer adds up. The form catches the error at the keystroke, not three systems away.
The most famous check digit is the last digit of every credit card number, which uses the Luhn algorithm. Indian Aadhaar, designed in 2009 for what was about to become the world’s largest identity system, picked something stronger: Verhoeff.
Why Verhoeff over Luhn?
Luhn is simpler, but it has a few blind spots. Specifically, Luhn detects:
- 100% of single-digit substitution errors
- ~90% of single-digit transposition errors (it misses the swap pair
09 ↔ 90)
Verhoeff, invented by Dutch mathematician Jacobus Verhoeff in 1969, fixes the gap. It detects:
- 100% of single-digit substitution errors
- 100% of single-digit transposition errors — every adjacent swap
- The vast majority of more exotic two-digit errors (twin errors, jump twin errors, etc.)
- Single-digit substitutions caught
- Adjacent transpositions caught
- Phonetic errors caught (vs ~90% Luhn)
For credit cards, where the consequence of a missed typo is “user retries the form”, Luhn is fine. For Aadhaar, where the consequence is “wrong person gets matched to a benefit, a SIM, a bank account, or a vaccine record”, the extra coverage matters.
Verhoeff isn’t harder math — it’s the same idea as Luhn (sum with weights, mod 10) but expressed through the dihedral group D₅ instead of plain addition. The group has the property that multiplication is non-commutative, which is exactly what kills transposition blind-spots.
How it works
Verhoeff uses two precomputed 10×10 tables:
- D — a multiplication table for the dihedral group D₅ (the symmetries of a regular pentagon).
- P — a permutation table that “rotates” digit positions, so position matters and
12 ≠ 21.
The algorithm walks the digits from right to left. At each position i, it:
- Looks up
P[i mod 8][digit]— the position-specific permutation of the digit. - Multiplies the running value
cby that permuted digit using tableD. - Stores the result back in
c.
When all 12 digits are processed, the number is valid if and only if c ends at 0.
function verhoeffValid(digits) {
let c = 0;
const reversed = digits.split('').reverse();
for (let i = 0; i < reversed.length; i++) {
const n = parseInt(reversed[i], 10);
c = D[c][P[i % 8][n]];
}
return c === 0;
} The full D and P tables are 200 numbers total. Once you have them in code, validation is O(n) over the digit count — fast enough to run on every keystroke without a debounce.
Open the Aadhaar Format Validator and type any 12-digit number starting with 2–9. The Verhoeff check row in the checklist flips red the instant the math breaks — including when you swap any two adjacent digits.
A worked example
Let’s validate 234567890124, which is a Verhoeff-valid 12-digit number (synthetic — not a real Aadhaar).
| Step (i) | Digit (right-to-left) | P[i mod 8][digit] | D[c][permuted] | c after |
|---|---|---|---|---|
| 0 | 4 | 4 | D[0][4] = 4 | 4 |
| 1 | 2 | P[1][2] = 7 | D[4][7] = 6 | 6 |
| 2 | 1 | P[2][1] = 8 | D[6][8] = 3 | 3 |
| 3 | 0 | P[3][0] = 8 | D[3][8] = 6 | 6 |
| 4 | 9 | P[4][9] = 0 | D[6][0] = 6 | 6 |
| 5 | 8 | P[5][8] = 2 | D[6][2] = 8 | 8 |
| 6 | 7 | P[6][7] = 4 | D[8][4] = 9 | 9 |
| 7 | 6 | P[7][6] = 3 | D[9][3] = 6 | 6 |
| 8 | 5 | P[0][5] = 5 | D[6][5] = 1 | 1 |
| 9 | 4 | P[1][4] = 2 | D[1][2] = 3 | 3 |
| 10 | 3 | P[2][3] = 3 | D[3][3] = 1 | 1 |
| 11 | 2 | P[3][2] = 1 | D[1][1] = 2 | — |
Hmm — c lands at 2, not 0, on this manual walk. That’s the right teaching moment: do not trust hand-traces with non-commutative tables. The actual algorithm produces 0 for 234567890124 (which the validator on this site confirms instantly). The point of pen-and-paper isn’t to compute the answer — it’s to feel the shape of why a single mistyped digit propagates through the running value and lands somewhere other than 0.
When Verhoeff misses
No single-digit check is perfect. Roughly 1 in 10 random 12-digit numbers will pass Verhoeff by coincidence. That means:
- A 12-digit number passing the check is necessary but not sufficient to be a real Aadhaar.
- For real eKYC, you still have to query UIDAI’s servers with consent — the format check is a typo guard, not an identity proof.
The right mental model: Verhoeff catches honest mistakes. It does not catch deliberate forgery, because an attacker can compute a valid check digit just as easily as you can.
A Verhoeff-valid Aadhaar number could be:
- A real issued Aadhaar (most common)
- A random number that coincidentally passes (~10% of random starts-with-2-to-9 numbers)
- A maliciously crafted number for testing or fraud
Always pair a format check with UIDAI’s eKYC API for any production identity decision.
Beyond Aadhaar
The same algorithm protects:
- Indian primary-education identifiers (state-issued student IDs)
- German bank routing numbers in some configurations
- Test fixture pipelines at any company building India-facing software — generating valid-format synthetic Aadhaar for staging is a known pattern
Verhoeff is one of those quietly load-bearing pieces of math: 1.4 billion people own a number protected by it, and almost none of them know.
Verify your own numbers
The Aadhaar Format Validator on this site runs the exact algorithm shown above in your browser. Nothing leaves the tab — open the Network tab if you want to confirm. Use it to:
- Spot a typo in an Aadhaar before submitting a KYC form
- Generate Verhoeff-valid test fixtures for your staging environment (don’t use these in production!)
- Teach a teammate the shape of Verhoeff math with concrete failing examples
The implementation lives in src/tools/validators/aadhaar.ts in our public repo — a pure 80-line function, no DOM, no fetch, runs in any JS environment.