The rules in one place
A string is a well-formed Aadhaar number when all of the following hold:
- After removing spaces (and dashes, if you accept them) it is exactly 12 ASCII digits.
- The first digit is 2–9. UIDAI never issues numbers starting with 0 or 1.
- The 12th digit is a valid Verhoeff check digit for the first 11.
Rules 1 and 2 are a regex. Rule 3 is not — a regex cannot compute a checksum, and about one in ten random 12-digit strings passes the checksum by chance, so skipping it lets most typos through. Full details of the algorithm are in the Verhoeff guide.
Aadhaar regex
Use this to check the shape before running the checksum:
^[2-9][0-9]{11}$
If you accept the printed format with spaces between groups:
^[2-9][0-9]{3}\s?[0-9]{4}\s?[0-9]{4}$
Prefer stripping whitespace first and using the plain pattern — it is simpler to reason about and avoids accepting a stray space in the middle of a group.
HTML form input
Use a numeric keyboard on phones, cap the length, and let the browser do a first shape check. Never set autocomplete to a value that lets the browser store the number.
<label for="aadhaar">Aadhaar number</label>
<input id="aadhaar" name="aadhaar" type="text"
inputmode="numeric" autocomplete="off"
pattern="[2-9][0-9]{3}\s?[0-9]{4}\s?[0-9]{4}"
maxlength="14" placeholder="XXXX XXXX XXXX"
title="12-digit Aadhaar number">
<!-- pattern catches the shape; run the Verhoeff check in JS before submit -->
Verhoeff validation in 10 languages
Each snippet is self-contained and dependency-free. Select a language, copy, and add a unit test using the test numbers.
// Verhoeff tables (dihedral group D5)
const d = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
];
const p = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
];
function verhoeffValid(digits) {
let c = 0;
const arr = String(digits).split('').reverse();
for (let i = 0; i < arr.length; i++) {
c = d[c][p[i % 8][Number(arr[i])]];
}
return c === 0;
}
/** True when s is a well-formed Aadhaar number (spaces allowed). */
function isValidAadhaar(input) {
const s = String(input).replace(/\s/g, '');
return /^[2-9]\d{11}$/.test(s) && verhoeffValid(s);
}
// isValidAadhaar('9999 4105 7058') === true (UIDAI sandbox number)
// isValidAadhaar('999941057059') === false (bad check digit)
const d: readonly (readonly number[])[] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
];
const p: readonly (readonly number[])[] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
];
export function verhoeffValid(digits: string): boolean {
let c = 0;
const arr = digits.split('').reverse();
for (let i = 0; i < arr.length; i++) {
c = d[c][p[i % 8][Number(arr[i])]];
}
return c === 0;
}
export function isValidAadhaar(input: string): boolean {
const s = input.replace(/\s/g, '');
return /^[2-9]\d{11}$/.test(s) && verhoeffValid(s);
}
import re
D = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
]
P = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
]
def verhoeff_valid(digits: str) -> bool:
c = 0
for i, ch in enumerate(reversed(digits)):
c = D[c][P[i % 8][int(ch)]]
return c == 0
def is_valid_aadhaar(value: str) -> bool:
"""True when value is a well-formed Aadhaar number (spaces allowed)."""
s = re.sub(r"\s", "", str(value))
return re.fullmatch(r"[2-9]\d{11}", s) is not None and verhoeff_valid(s)
# is_valid_aadhaar("9999 4105 7058") -> True (UIDAI sandbox number)
# is_valid_aadhaar("999941057059") -> False (bad check digit)
import java.util.regex.Pattern;
public final class Aadhaar {
private static final int[][] D = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{1, 2, 3, 4, 0, 6, 7, 8, 9, 5},
{2, 3, 4, 0, 1, 7, 8, 9, 5, 6},
{3, 4, 0, 1, 2, 8, 9, 5, 6, 7},
{4, 0, 1, 2, 3, 9, 5, 6, 7, 8},
{5, 9, 8, 7, 6, 0, 4, 3, 2, 1},
{6, 5, 9, 8, 7, 1, 0, 4, 3, 2},
{7, 6, 5, 9, 8, 2, 1, 0, 4, 3},
{8, 7, 6, 5, 9, 3, 2, 1, 0, 4},
{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
};
private static final int[][] P = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{1, 5, 7, 6, 2, 8, 3, 0, 9, 4},
{5, 8, 0, 3, 7, 9, 6, 1, 4, 2},
{8, 9, 1, 6, 0, 4, 3, 5, 2, 7},
{9, 4, 5, 3, 1, 2, 6, 8, 7, 0},
{4, 2, 8, 6, 5, 7, 3, 9, 0, 1},
{2, 7, 9, 3, 8, 0, 6, 4, 1, 5},
{7, 0, 4, 6, 9, 1, 3, 2, 5, 8}
};
private static final Pattern SHAPE = Pattern.compile("[2-9]\\d{11}");
private Aadhaar() {}
/** True when input is a well-formed Aadhaar number (spaces allowed). */
public static boolean isValid(String input) {
if (input == null) return false;
String s = input.replaceAll("\\s", "");
if (!SHAPE.matcher(s).matches()) return false;
int c = 0;
int len = s.length();
for (int i = 0; i < len; i++) {
int digit = s.charAt(len - 1 - i) - '0';
c = D[c][P[i % 8][digit]];
}
return c == 0;
}
}
using System.Text.RegularExpressions;
public static class Aadhaar
{
private static readonly int[][] D =
{
new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
new[] {1, 2, 3, 4, 0, 6, 7, 8, 9, 5},
new[] {2, 3, 4, 0, 1, 7, 8, 9, 5, 6},
new[] {3, 4, 0, 1, 2, 8, 9, 5, 6, 7},
new[] {4, 0, 1, 2, 3, 9, 5, 6, 7, 8},
new[] {5, 9, 8, 7, 6, 0, 4, 3, 2, 1},
new[] {6, 5, 9, 8, 7, 1, 0, 4, 3, 2},
new[] {7, 6, 5, 9, 8, 2, 1, 0, 4, 3},
new[] {8, 7, 6, 5, 9, 3, 2, 1, 0, 4},
new[] {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
};
private static readonly int[][] P =
{
new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
new[] {1, 5, 7, 6, 2, 8, 3, 0, 9, 4},
new[] {5, 8, 0, 3, 7, 9, 6, 1, 4, 2},
new[] {8, 9, 1, 6, 0, 4, 3, 5, 2, 7},
new[] {9, 4, 5, 3, 1, 2, 6, 8, 7, 0},
new[] {4, 2, 8, 6, 5, 7, 3, 9, 0, 1},
new[] {2, 7, 9, 3, 8, 0, 6, 4, 1, 5},
new[] {7, 0, 4, 6, 9, 1, 3, 2, 5, 8}
};
private static readonly Regex Shape = new Regex(@"^[2-9]\d{11}$", RegexOptions.Compiled);
/// <summary>True when input is a well-formed Aadhaar number (spaces allowed).</summary>
public static bool IsValid(string? input)
{
if (input is null) return false;
var s = Regex.Replace(input, @"\s", "");
if (!Shape.IsMatch(s)) return false;
var c = 0;
for (var i = 0; i < s.Length; i++)
{
var digit = s[s.Length - 1 - i] - '0';
c = D[c][P[i % 8][digit]];
}
return c == 0;
}
}
<?php
function isValidAadhaar(string $input): bool
{
static $d = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
];
static $p = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
];
$s = preg_replace('/\s/', '', $input);
if (!preg_match('/^[2-9]\d{11}$/', $s)) {
return false;
}
$c = 0;
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
$digit = (int) $s[$len - 1 - $i];
$c = $d[$c][$p[$i % 8][$digit]];
}
return $c === 0;
}
// isValidAadhaar('9999 4105 7058') === true
package aadhaar
import "regexp"
var d = [10][10]int{
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{1, 2, 3, 4, 0, 6, 7, 8, 9, 5},
{2, 3, 4, 0, 1, 7, 8, 9, 5, 6},
{3, 4, 0, 1, 2, 8, 9, 5, 6, 7},
{4, 0, 1, 2, 3, 9, 5, 6, 7, 8},
{5, 9, 8, 7, 6, 0, 4, 3, 2, 1},
{6, 5, 9, 8, 7, 1, 0, 4, 3, 2},
{7, 6, 5, 9, 8, 2, 1, 0, 4, 3},
{8, 7, 6, 5, 9, 3, 2, 1, 0, 4},
{9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
}
var p = [8][10]int{
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{1, 5, 7, 6, 2, 8, 3, 0, 9, 4},
{5, 8, 0, 3, 7, 9, 6, 1, 4, 2},
{8, 9, 1, 6, 0, 4, 3, 5, 2, 7},
{9, 4, 5, 3, 1, 2, 6, 8, 7, 0},
{4, 2, 8, 6, 5, 7, 3, 9, 0, 1},
{2, 7, 9, 3, 8, 0, 6, 4, 1, 5},
{7, 0, 4, 6, 9, 1, 3, 2, 5, 8},
}
var shape = regexp.MustCompile(`^[2-9][0-9]{11}$`)
var space = regexp.MustCompile(`\s`)
// IsValid reports whether input is a well-formed Aadhaar number (spaces allowed).
func IsValid(input string) bool {
s := space.ReplaceAllString(input, "")
if !shape.MatchString(s) {
return false
}
c := 0
n := len(s)
for i := 0; i < n; i++ {
digit := int(s[n-1-i] - '0')
c = d[c][p[i%8][digit]]
}
return c == 0
}
object Aadhaar {
private val d = arrayOf(
intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
intArrayOf(1, 2, 3, 4, 0, 6, 7, 8, 9, 5),
intArrayOf(2, 3, 4, 0, 1, 7, 8, 9, 5, 6),
intArrayOf(3, 4, 0, 1, 2, 8, 9, 5, 6, 7),
intArrayOf(4, 0, 1, 2, 3, 9, 5, 6, 7, 8),
intArrayOf(5, 9, 8, 7, 6, 0, 4, 3, 2, 1),
intArrayOf(6, 5, 9, 8, 7, 1, 0, 4, 3, 2),
intArrayOf(7, 6, 5, 9, 8, 2, 1, 0, 4, 3),
intArrayOf(8, 7, 6, 5, 9, 3, 2, 1, 0, 4),
intArrayOf(9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
)
private val p = arrayOf(
intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
intArrayOf(1, 5, 7, 6, 2, 8, 3, 0, 9, 4),
intArrayOf(5, 8, 0, 3, 7, 9, 6, 1, 4, 2),
intArrayOf(8, 9, 1, 6, 0, 4, 3, 5, 2, 7),
intArrayOf(9, 4, 5, 3, 1, 2, 6, 8, 7, 0),
intArrayOf(4, 2, 8, 6, 5, 7, 3, 9, 0, 1),
intArrayOf(2, 7, 9, 3, 8, 0, 6, 4, 1, 5),
intArrayOf(7, 0, 4, 6, 9, 1, 3, 2, 5, 8)
)
private val shape = Regex("[2-9]\\d{11}")
/** True when input is a well-formed Aadhaar number (spaces allowed). */
fun isValid(input: String): Boolean {
val s = input.replace(Regex("\\s"), "")
if (!shape.matches(s)) return false
var c = 0
s.reversed().forEachIndexed { i, ch -> c = d[c][p[i % 8][ch - '0']] }
return c == 0
}
}
enum Aadhaar {
private static let d: [[Int]] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
]
private static let p: [[Int]] = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
]
/// True when input is a well-formed Aadhaar number (spaces allowed).
static func isValid(_ input: String) -> Bool {
let s = input.filter { !$0.isWhitespace }
guard s.count == 12,
s.allSatisfy({ $0.isASCII && $0.isNumber }),
let first = s.first, first != "0", first != "1" else { return false }
var c = 0
for (i, ch) in s.reversed().enumerated() {
let digit = Int(String(ch))!
c = d[c][p[i % 8][digit]]
}
return c == 0
}
}
const _d = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
];
const _p = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
];
/// True when [input] is a well-formed Aadhaar number (spaces allowed).
bool isValidAadhaar(String input) {
final s = input.replaceAll(RegExp(r'\s'), '');
if (!RegExp(r'^[2-9]\d{11}$').hasMatch(s)) return false;
var c = 0;
final n = s.length;
for (var i = 0; i < n; i++) {
final digit = s.codeUnitAt(n - 1 - i) - 48;
c = _d[c][_p[i % 8][digit]];
}
return c == 0;
}
Generating check digits and test data
Generation is the same loop with the position shifted by one, followed by the inverse table. Use it only for test fixtures — never to fabricate identity data.
const inv = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
// Check digit to append to an 11-digit body.
function verhoeffCheckDigit(body) {
let c = 0;
const arr = String(body).split('').reverse();
for (let i = 0; i < arr.length; i++) {
c = d[c][p[(i + 1) % 8][Number(arr[i])]]; // note the +1
}
return inv[c];
}
// Random, checksum-valid test number. First digit 2–9.
function testAadhaar() {
let body = String(2 + Math.floor(Math.random() * 8));
for (let i = 0; i < 10; i++) body += Math.floor(Math.random() * 10);
return body + verhoeffCheckDigit(body);
}
Ready-made numbers, including the ones UIDAI publishes for its sandbox, are on the test Aadhaar numbers page.
Aadhaar Virtual ID (16 digits)
A VID is a 16-digit number that can be used in place of an Aadhaar number for authentication. It uses the same Verhoeff checksum, so the same verhoeffValid function works — only the length and regex change: ^[2-9][0-9]{15}$. If your form accepts both, branch on length. See the VID validator.
Handling Aadhaar numbers responsibly
Validation is the easy part. Under the Aadhaar Act and the Digital Personal Data Protection Act, 2023, the number itself is sensitive. A practical checklist:
- Validate client-side first so typos are caught before the number is ever transmitted.
- Mask on display: show only the last four digits (
XXXX XXXX 7058), which is how UIDAI's own "masked Aadhaar" works. - Never log the full number. Scrub it from application logs, crash reports and analytics events.
- Encrypt at rest and restrict who can decrypt. Store a hash or a reference token if you only need to match, not read.
- Use test numbers in non-production. Real Aadhaar numbers do not belong in staging databases or fixtures.
- Don't treat a checksum pass as verification. For KYC, use UIDAI-authorised authentication or offline e-KYC. See how to properly verify an Aadhaar number.
Excel and Google Sheets
There is no built-in spreadsheet function for Verhoeff and the formula version is unreadable. Paste your column into the bulk validator instead; it runs locally and exports a CSV you can paste back.