GSTIN Validation in Node.js — Indian Tax ID Structure Explained
India's Goods and Services Tax Identification Number is a 15-character alphanumeric identifier with embedded state codes, PAN references, and a check character. Here's how to validate it properly — and why a single regex is not enough.
In this guide
1. What is a GSTIN?
The Goods and Services Tax Identification Number (GSTIN) is the unique tax identifier assigned to every business registered under India's GST regime. GST registration is mandatory for businesses with an annual turnover exceeding ₹20 lakh (₹10 lakh for special category states). Every inter-state transaction, e-commerce seller, and input-tax-credit claim requires a valid GSTIN.
A GSTIN is a 15-character alphanumeric string — not a random code, but a structured identifier that encodes the state of registration, the PAN (Permanent Account Number) of the entity, an entity serial number, and a check character. For example: 27AAPFU0939F1ZV.
If your Node.js application handles Indian B2B invoicing, GST filing, or supply-chain compliance, you need to validate GSTINs — and format-checking alone will not cut it.
2. GSTIN anatomy — 15 characters decoded
Every GSTIN follows the same structure. Let's break down 27AAPFU0939F1ZV:
| Position | Characters | Meaning | Example |
|---|---|---|---|
| 1–2 | 2 digits | State code (01–37, matching Indian state/UT codes) | 27 |
| 3–12 | 10 alphanumeric | PAN of the entity (Permanent Account Number) | AAPFU0939F |
| 13 | 1 alphanumeric | Entity number (1–9 for multiple registrations under same PAN, Z as default) | 1 |
| 14 | 1 character | Default character — always Z | Z |
| 15 | 1 alphanumeric | Check character (mod-36 based checksum) | V |
State codes
The first two digits identify the state or union territory. Here are some common ones:
| Code | State / UT | Code | State / UT |
|---|---|---|---|
| 01 | Jammu & Kashmir | 19 | West Bengal |
| 02 | Himachal Pradesh | 21 | Odisha |
| 06 | Haryana | 23 | Madhya Pradesh |
| 07 | Delhi | 24 | Gujarat |
| 08 | Rajasthan | 27 | Maharashtra |
| 09 | Uttar Pradesh | 29 | Karnataka |
| 10 | Bihar | 32 | Kerala |
| 12 | Arunachal Pradesh | 33 | Tamil Nadu |
| 18 | Assam | 36 | Telangana |
| 37 | Andhra Pradesh |
3. The check character algorithm
The 15th character of a GSTIN is a check character computed using a mod-36 algorithm. This is similar in concept to Luhn but operates over the full alphanumeric character set (0–9, A–Z = 36 characters). The algorithm works as follows:
- Take the first 14 characters of the GSTIN. Map each character to its position in the set
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ(0–35). - For each character at position i (1-indexed), multiply its code-point value by the position factor. Compute
value * (i), then split the product into quotient and remainder when divided by 36. - Sum all the (quotient + remainder) pairs across 14 characters.
- Compute
remainder = total % 36, then the check value is(36 - remainder) % 36. - Map the check value back to a character (0–9 stay as digits, 10–35 become A–Z).
Here is a reference implementation in JavaScript:
// Reference implementation — DO NOT use in production without thorough testing const CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; function gstinCheckCharacter(gstin14) { let total = 0; for (let i = 0; i < 14; i++) { const charIndex = CHARSET.indexOf(gstin14[i].toUpperCase()); if (charIndex === -1) return null; // invalid character const product = charIndex * (i + 1); const quotient = Math.floor(product / 36); const remainder = product % 36; total += quotient + remainder; } const mod = total % 36; const checkValue = (36 - mod) % 36; return CHARSET[checkValue]; } // Example gstinCheckCharacter('27AAPFU0939F1Z'); // → "V" gstinCheckCharacter('29AABCT1332L1Z'); // → "1"
4. Why manual validation isn't enough
State code validation
There are 37 valid state/UT codes, but the range is not contiguous — codes like 00, 11, 13, 14, 15, 16, 17, 25, 26, 28, 31, 34, and 35 are either unused or reserved. A naive 01–37 range check will accept invalid codes. You need an explicit allowlist.
PAN structure within the GSTIN
Positions 3–12 must form a valid PAN: five uppercase letters, four digits, one uppercase letter. The 4th letter encodes the taxpayer category (C, P, F, H, A, T, B, L, J, G). Validating this sub-structure requires more than a basic regex.
Entity type derivation
The entity number at position 13 can be 1–9 or a letter (Z for default first registration). Multiple GSTINs can exist under the same PAN in different states. Knowing whether a GSTIN belongs to a firm, company, or individual requires parsing the embedded PAN correctly.
Check character calculation
The mod-36 check character is essential for catching typos and transposition errors. Skipping it means your application will accept structurally plausible but mathematically invalid GSTINs — numbers that were never issued by GSTN.
5. The right solution: one API call
Instead of implementing state-code lookups, PAN sub-validation, and the mod-36 checksum yourself, use the IsValid GSTIN API. A single GET request validates the full structure, verifies the check character, and returns parsed fields — state name, PAN, entity type, and more.
Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.
Full parameter reference and response schema: GSTIN Validation API docs →
6. Node.js code example
Using the IsValid SDK (country-specific namespace) or the native fetch API.
import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Validate a GSTIN ──────────────────────────────────────────────────────── const result = await iv.in.gstin('27AAPFU0939F1ZV'); if (!result.valid) { console.log('Invalid GSTIN'); } else { console.log(`Valid GSTIN from ${result.stateName}`); console.log(`PAN: ${result.pan}`); console.log(`Entity type: ${result.entityType}`); }
iv.in.gstin(). This groups all India-specific validators (GSTIN, PAN, Aadhaar, etc.) under a single namespace.7. cURL example
Validate a GSTIN directly from the terminal:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/in/gstin?value=27AAPFU0939F1ZV"
Test with an invalid GSTIN (wrong check character):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/in/gstin?value=27AAPFU0939F1ZX"
Test with an invalid state code:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/in/gstin?value=99AAPFU0939F1ZV"
8. Understanding the response
Response for a valid GSTIN:
{ "valid": true, "gstin": "27AAPFU0939F1ZV", "stateCode": "27", "stateName": "Maharashtra", "pan": "AAPFU0939F", "entityNumber": "1", "entityType": "Firm", "checkChar": "V" }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the GSTIN is structurally valid with correct check character |
| gstin | string | The normalized (uppercased) GSTIN |
| stateCode | string | 2-digit Indian state or union territory code |
| stateName | string | Full name of the state or UT (e.g. "Maharashtra") |
| pan | string | Embedded 10-character PAN extracted from the GSTIN |
| entityNumber | string | Registration serial number under this PAN (1–9 or Z) |
| entityType | string | Derived from PAN — Company, Person, Firm, HUF, AOP, Trust, etc. |
| checkChar | string | The 15th check character |
When the GSTIN is invalid, valid is false and the parsed fields may be absent or partial depending on which part of the structure failed validation.
9. Edge cases
(a) Multiple GSTINs per PAN
A single business entity (identified by PAN) can hold multiple GSTIN registrations — one per state where it operates. For example, a company registered in Maharashtra and Karnataka will have two GSTINs that share the same PAN (positions 3–12) but differ in the state code (positions 1–2) and entity number (position 13). Do not assume a PAN maps to a single GSTIN.
// Same PAN, different states const mh = await iv.in.gstin('27AAPFU0939F1ZV'); // Maharashtra const ka = await iv.in.gstin('29AAPFU0939F1ZB'); // Karnataka console.log(mh.pan === ka.pan); // true — same entity, different states
(b) State code changes — Telangana
When Telangana was carved out of Andhra Pradesh in 2014, it received state code 36. Andhra Pradesh retained code 37. Legacy GSTINs from unified AP may still circulate. Your validation logic should accept code 36 (Telangana) as a valid, distinct state — not treat it as an anomaly.
(c) Input normalization
Users may enter GSTINs with spaces, hyphens, or lowercase letters: 27 AAPF U0939F 1ZV, 27-AAPFU0939F-1ZV, or 27aapfu0939f1zv. The API strips whitespace and hyphens, and uppercases the input automatically. Pass the raw user input — no need to sanitize beforehand.
// All of these produce the same result await iv.in.gstin('27AAPFU0939F1ZV'); await iv.in.gstin('27 aapfu0939f 1zv'); await iv.in.gstin('27-AAPFU0939F-1ZV');
Network failures in your code
Always wrap the API call in a try/catch. A network timeout should not cause your invoice flow to crash — decide upfront whether to fail open or closed on API unavailability.
async function validateGstinSafe(gstin) { try { return await iv.in.gstin(gstin); } catch (err) { // Log and decide: fail open (allow) or fail closed (reject) logger.error('GSTIN validation failed:', err.message); return { valid: null, error: 'validation_unavailable' }; } }
10. Summary
See also
Validate GSTIN numbers instantly
Free tier includes 100 API calls per day. No credit card required. Full structure validation with parsed state, PAN, and entity type.