Regex for Indian ID & Document Validation
Every major Indian identity document — Aadhaar, PAN, GSTIN, IFSC, passport, phone number, PIN code, driving license — has a fixed, documented format, which makes them some of the most regex-friendly IDs in the world. But they split into two genuinely different problems: some formats are protected by a checksum digit that regex cannot verify, and some are pure format, where a regex match is the whole validation. Confusing the two is the single most common mistake in KYC and signup forms — this guide draws the line clearly, then gives you a tested pattern for each.
All 8 formats at a glance
Vehicle registration numbers get their own dedicated guide since the RTO-series and newer BH-series formats need more room than a table row — everything else is here.
| ID | Length | Checksum? | Pattern |
|---|---|---|---|
| Aadhaar | 12 digits | Yes (Verhoeff) | /^[2-9][0-9]{11}$/ |
| PAN | 10 chars | Yes (check letter) | /^[A-Z]{5}[0-9]{4}[A-Z]$/ |
| GSTIN | 15 chars | Yes (check char) | /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/ |
| IFSC | 11 chars | No (fixed 5th char) | /^[A-Z]{4}0[A-Z0-9]{6}$/ |
| Phone | 10–13 chars | No | /^(?:\+91|91)?[6-9][0-9]{9}$/ |
| PIN code | 6 digits | No | /^[1-9][0-9]{5}$/ |
| Passport | 8 chars | No | /^[A-Z][0-9]{7}$/ |
| Driving license | 15–16 chars | No | /^[A-Z]{2}[0-9]{2}[ -]?(19|20)[0-9]{2}[0-9]{7}$/ |
Checksum-protected IDs
These four all embed a check character — a digit or letter mathematically derived from the rest of the number. Regex confirms the shape is right; it cannot confirm the check character is mathematically valid, so a random string that happens to fit the pattern will still pass.
Aadhaar (12 digits)
UIDAI never issues an Aadhaar number starting with 0 or 1, and the last digit is a Verhoeff-algorithm checksum — a scheme specifically designed to catch transposed and adjacent-digit typos, which is why UIDAI uses it instead of a simple digit sum.
const aadhaarRegex = /^[2-9][0-9]{11}$/;
aadhaarRegex.test('234567890123'); // true — shape is valid
// A regex pass here does NOT mean the number was ever issued.PAN (10 characters)
The 4th letter isn't random — it encodes the holder type (P for an individual, C for a company, H for a Hindu Undivided Family, and several others), so ABCPD1234E and ABCCD1234E are structurally different kinds of PAN, not just different numbers.
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
panRegex.test('ABCPD1234E'); // trueGSTIN (15 characters)
A GSTIN is the most structured Indian ID because it isn't really its own identifier — it's a 2-digit state code (01 for Jammu & Kashmir through 38 for Ladakh) wrapped around a full 10-character PAN, followed by an entity count, the fixed letter Z, and a checksum. Example: 27AAPFU0939F1ZV.
const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
gstinRegex.test('27AAPFU0939F1ZV'); // true
gstinRegex.test('27AAPFU0939F1XV'); // false — 14th char must be ZIFSC (11 characters)
Grouped here for its fixed 5th character rather than a true mathematical checksum: RBI reserves that position as the digit 0 for every bank, so it acts as a structural sanity check even though it isn't derived from the surrounding characters.
const ifscRegex = /^[A-Z]{4}0[A-Z0-9]{6}$/;
ifscRegex.test('HDFC0001234'); // trueFormat-only IDs
No embedded checksum in any of these four — a regex match genuinely is a complete structural validation. What still varies is exactly which characters are legal in which position, which is where copy-pasted "close enough" patterns tend to go wrong.
Mobile phone (10–13 characters)
India's Department of Telecommunications only allocates mobile numbers starting with 6, 7, 8, or 9 — a number starting with 5 or below is never a valid Indian mobile number, regardless of length.
const phoneRegex = /^(?:\+91|91)?[6-9][0-9]{9}$/;
phoneRegex.test('+919876543210'); // true
phoneRegex.test('9876543210'); // true
phoneRegex.test('5876543210'); // false — DoT never allocates a leading 5PIN code (6 digits)
The first digit is the postal zone (1 through 9 — there is no zone 0), the second is the sub-zone, the third is the sorting district, and the last three identify the specific post office.
const pinRegex = /^[1-9][0-9]{5}$/;
pinRegex.test('110001'); // true — New Delhi GPOPassport (8 characters)
The simplest format on this list — one uppercase letter, seven digits, nothing more.
const passportRegex = /^[A-Z][0-9]{7}$/;
passportRegex.test('A1234567'); // trueDriving license (15–16 characters)
The only ID here without one nationwide format — the state code (MH, DL, KA, and so on), RTO code, and separator vary enough by issuing state that this pattern should be treated as "matches the common case," not a guarantee.
const dlRegex = /^[A-Z]{2}[0-9]{2}[ -]?(19|20)[0-9]{2}[0-9]{7}$/;
dlRegex.test('MH1220230012345'); // trueOne validator for all eight
Instead of eight separate copy-pasted functions, a single dispatcher keeps every pattern in one place — easier to update if a format changes, and it forces you to name the ID type explicitly at every call site instead of guessing from context.
const INDIAN_ID_PATTERNS = {
aadhaar: /^[2-9][0-9]{11}$/,
pan: /^[A-Z]{5}[0-9]{4}[A-Z]$/,
gstin: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/,
ifsc: /^[A-Z]{4}0[A-Z0-9]{6}$/,
phone: /^(?:\+91|91)?[6-9][0-9]{9}$/,
pinCode: /^[1-9][0-9]{5}$/,
passport: /^[A-Z][0-9]{7}$/,
drivingLicense: /^[A-Z]{2}[0-9]{2}[ -]?(19|20)[0-9]{2}[0-9]{7}$/,
};
function isValidIndianId(type, value) {
const pattern = INDIAN_ID_PATTERNS[type];
if (!pattern) throw new Error(`Unknown ID type: ${type}`);
return pattern.test(String(value).trim().toUpperCase());
}
isValidIndianId('pan', 'abcpd1234e'); // true
isValidIndianId('gstin', '27AAPFU0939F1ZV'); // true
isValidIndianId('aadhaar', '123456789012'); // false — starts with 1Common Mistakes
- Treating a regex pass as proof the ID is real. For Aadhaar, PAN, GSTIN, and IFSC, a regex match is a necessary but not sufficient check — the checksum (or fixed digit) still needs separate verification if it matters for your use case.
- Forgetting to normalize case before testing. All eight formats use uppercase letters; users routinely type PAN or IFSC in lowercase, so
.toUpperCase()before testing avoids false negatives. - Not trimming whitespace. Copy-pasted Aadhaar numbers often carry the UIDAI-style spacing (
2345 6789 0123) — strip spaces before testing, don't bake them into the regex unless you specifically want to accept that format. - Assuming one driving license format covers every state. The pattern here matches the common case; some states use different separators or field orders, so treat a non-match as "needs manual review," not "definitely invalid."
- Skipping the DoT leading-digit rule for phone numbers.
^[0-9]{10}$alone accepts numbers starting with 0–5, which no telecom operator has ever issued as a mobile number.
Frequently Asked Questions
No. Regex confirms the shape — length, and which characters are allowed where — but Aadhaar, PAN, and GSTIN all end in a checksum computed by an algorithm regex cannot run. A fabricated number that happens to fit the pattern will still pass. Passport, phone, and PIN code have no checksum, so for those, regex format validation is the whole job.
^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$ — a 2-digit state code, a full 10-character PAN embedded inside it, an entity number, the fixed letter Z, and a checksum character.
^(?:\+91|91)?[6-9][0-9]{9}$ — an optional +91 or 91 prefix, then 10 digits starting with 6, 7, 8, or 9, the only leading digits DoT allocates for mobile numbers.
No. Even checksum validation only confirms the number is well-formed, not that it was actually issued or is still active. For anything transaction-critical, confirm status against the official GST Portal or a PAN verification API instead of trusting format validation alone.