indycheck — Offline Indian ID validator

Validate PAN, Aadhaar, GSTIN, IFSC, UPI IDs, mobile numbers, PIN codes, TAN, CIN, LLPIN, Voter IDs and vehicle numbers — instantly, on-device, with no network call and no data leaving your server. One Rust core, bindings for JavaScript, TypeScript, Python and Rust.

New in v0.2.0: Bharat (BH) series vehicle numbers — 22BH1234AA now validates. Read the Vehicle docs →· Full changelog →

Install

npm install indycheck
import { validatePan } from 'indycheck';

const r = validatePan('AAAPL1234C');
console.log(r.ok);        // true
console.log(r.parsed);    // { entity: 'Individual', entity_code: 'P' }
console.log(r.reason_hi); // 'मान्य प्रारूप।'

Try it — right here, offline

Try: · · · · · ·

This demo runs 100% in your browser — the value you type is never sent anywhere.Open the full playground →

Why indycheck

🪪 12 validators

PAN, Aadhaar, GSTIN, IFSC, UPI, mobile, PIN, TAN, CIN, LLPIN, Voter ID (EPIC) and vehicle numbers.

🔌 Fully offline

No API keys, no network calls, no per-check fees. Works in a Lambda, a CLI, or a browser with no internet.

📦 Zero dependencies

One small Rust core compiled to native, Node and Python. Nothing to audit, nothing to break on install.

🇮🇳 Hindi + English reasons

Every failure comes with reason_en and reason_hi, so your form errors make sense to every user in India.

🎛️ Strict / lenient options

Control spaces, hyphens, +91 normalisation and uppercasing per call — strict for APIs, lenient for messy user input.

🎭 Masking & formatting

mask('aadhaar', …) → XXXX XXXX 0124, format('mobile', …) → +91 98765 43210. Display-safe by default.

🦀 Rust core

One implementation, one test suite — the same rules in JavaScript, Python and Rust. No drift between ports.

⚡ Batch-friendly

Validate thousands of rows in milliseconds. Built for CSV imports, signup forms and KYC pre-checks.

🎲 Test-data generators

generate('pan') gives you a valid-looking value for tests and seed data — clearly labelled TEST DATA ONLY, never a real ID.

🔍 Structured output

Every result returns a stable error code, a parsed breakdown (state, holder type, bank code…) and the normalised value.

🧩 Framework integrations

Ready-made Zod schemas, React Hook Form / Yup resolvers and Pydantic types — drop indycheck into the form library you already use.

⌨️ CLI

indycheck validate, detect and generate — check a single value or a whole CSV from your terminal.

Supported IDs

IDFormatChecksumExample
PAN5 letters + 4 digits + 1 letterFormat only — entity reported, not enforcedAAAPL1234C
Aadhaar12 digits, first digit 2–9Verhoeff234567890124
GSTIN15 chars: state + PAN + entity + Z + checkMod-36 Luhn27AAAPL1234C1ZE
IFSC4-letter bank code + 0 + 6 charsFormat only — no public checksumSBIN0001234
UPI IDhandle@bankFormat only — no public checksum9876543210@paytm
Mobile10 digits starting 6–9 (+91 ok)Format only — no public checksum9876543210
PIN code6 digits, first digit 1–9Format only — no public checksum781001
TAN4 letters + 5 digits + 1 letterFormat only — no public checksumMUMB12345C
CIN21 chars: L/U + industry + state + year + type + no.Format only — no public checksumU12345DL2025PTC123456
LLPIN3 letters + 4 digits (AAA-1234)Format only — no public checksumAAA-1234
Voter ID (EPIC)3 letters + 7 digitsFormat only — no public checksumABC1234567
VehicleState format (8–10 chars) or Bharat (BH) series YYBH####XXFormat only — no public checksumDL01AB1234 · 22BH1234AA

Error codes — machine-readable, stable across releases

CodeMeansExample
VALIDFully validAAAPL1234C
INVALID_FORMATCharacters or pattern don't matchABCDE1234 as PAN
INVALID_LENGTHWrong lengthAAAPL123C as PAN
INVALID_CHECKSUMVerhoeff / Mod-36 Luhn check failed234567890123 as Aadhaar
INVALID_FIRST_DIGITFirst digit not allowed5123456789 as mobile
INVALID_STATE_CODEUnknown state code99AAAPL1234C1ZE as GSTIN
INVALID_HANDLEUPI handle missing or unknownrahul@
UNKNOWN_TYPENo known ID format fits (detect only)hello world

Branch on code, never on the reason text. Every failure also carries a Hindi reason (reason_hi). Full error-code table →

⚠️ Honest limits — read this first

indycheck validates format and checksum only. It does not — and cannot — tell you whether an ID is real, active, or belongs to the person who typed it.