Guide · Python · SDK · REST API

GSTIN Validation in Python — 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.

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 Python 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:

PositionCharactersMeaningExample
1–22 digitsState code (01–37, matching Indian state/UT codes)27
3–1210 alphanumericPAN of the entity (Permanent Account Number)AAPFU0939F
131 alphanumericEntity number (1–9 for multiple registrations under same PAN, Z as default)1
141 characterDefault character — always ZZ
151 alphanumericCheck character (mod-36 based checksum)V

State codes

The first two digits identify the state or union territory. Here are some common ones:

CodeState / UTCodeState / UT
01Jammu & Kashmir19West Bengal
02Himachal Pradesh21Odisha
06Haryana23Madhya Pradesh
07Delhi24Gujarat
08Rajasthan27Maharashtra
09Uttar Pradesh29Karnataka
10Bihar32Kerala
12Arunachal Pradesh33Tamil Nadu
18Assam36Telangana
37Andhra Pradesh
ℹ️The 4th character of the embedded PAN (position 6 in the GSTIN) encodes the entity type: C = Company, P = Person, F = Firm, H = HUF, A = AOP, T = Trust, and others. This lets you derive the entity type directly from the GSTIN.

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:

  1. Take the first 14 characters of the GSTIN. Map each character to its position in the set 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ (0–35).
  2. 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.
  3. Sum all the (quotient + remainder) pairs across 14 characters.
  4. Compute remainder = total % 36, then the check value is (36 - remainder) % 36.
  5. Map the check value back to a character (0–9 stay as digits, 10–35 become A–Z).

Here is a reference implementation in Python:

# Reference implementation — DO NOT use in production without thorough testing
CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"


def gstin_check_character(gstin_14: str) -> str | None:
    """Compute the GSTIN check character for the first 14 characters."""
    total = 0

    for i, ch in enumerate(gstin_14.upper()):
        char_index = CHARSET.find(ch)
        if char_index == -1:
            return None  # invalid character

        product = char_index * (i + 1)
        quotient, remainder = divmod(product, 36)
        total += quotient + remainder

    mod = total % 36
    check_value = (36 - mod) % 36
    return CHARSET[check_value]


# Examples
gstin_check_character("27AAPFU0939F1Z")  # → "V"
gstin_check_character("29AABCT1332L1Z")  # → "1"
⚠️The exact algorithm used by GSTN has subtle implementation details. Different sources describe slightly different weighting schemes. For production use, rely on the API rather than hand-rolling the check digit — the government portal is the source of truth.

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.

Full
Validation
structure + checksum
<50ms
Response time
instant validation
100/day
Free tier
no credit card

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. Python code example

Using the isvalid-sdk Python SDK (country-specific namespace) or the requests library. Install with pip install isvalid-sdk or pip install requests.

# gstin_validator.py
import os
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))

# ── Validate a GSTIN ────────────────────────────────────────────────────────
# Note: "in_" because "in" is a reserved keyword in Python

result = iv.in_.gstin("27AAPFU0939F1ZV")

if not result["valid"]:
    print("Invalid GSTIN")
else:
    print(f"Valid GSTIN from {result['stateName']}")
    print(f"PAN: {result['pan']}")
    print(f"Entity type: {result['entityType']}")

In a Django REST Framework view, you might use it like this:

# views.py (Django REST Framework)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from gstin_validator import validate_gstin
import requests as http_requests


class InvoiceView(APIView):
    def post(self, request):
        gstin = request.data.get("gstin")
        if not gstin:
            return Response(
                {"error": "gstin is required"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            result = validate_gstin(gstin)
        except http_requests.RequestException:
            return Response(
                {"error": "GSTIN validation service unavailable"},
                status=status.HTTP_502_BAD_GATEWAY,
            )

        if not result["valid"]:
            return Response(
                {"error": "Invalid GSTIN"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        return Response({
            "valid": True,
            "state": result["stateName"],
            "entityType": result["entityType"],
        })

Or with Flask:

# app.py (Flask)
from flask import Flask, request, jsonify
from gstin_validator import validate_gstin

app = Flask(__name__)


@app.post("/validate-gstin")
def validate():
    data = request.get_json()

    try:
        result = validate_gstin(data["gstin"])
    except requests.RequestException:
        return jsonify(error="GSTIN validation service unavailable"), 502

    if not result["valid"]:
        return jsonify(error="Invalid GSTIN"), 400

    return jsonify(
        valid=True,
        state=result["stateName"],
        pan=result["pan"],
        entity_type=result["entityType"],
    )
The Python SDK uses iv.in_.gstin() with an underscore — because in is a reserved keyword in Python. The Node.js SDK uses iv.in.gstin() without the underscore.

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"
}
FieldTypeDescription
validbooleanWhether the GSTIN is structurally valid with correct check character
gstinstringThe normalized (uppercased) GSTIN
stateCodestring2-digit Indian state or union territory code
stateNamestringFull name of the state or UT (e.g. "Maharashtra")
panstringEmbedded 10-character PAN extracted from the GSTIN
entityNumberstringRegistration serial number under this PAN (1–9 or Z)
entityTypestringDerived from PAN — Company, Person, Firm, HUF, AOP, Trust, etc.
checkCharstringThe 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
mh = iv.in_.gstin("27AAPFU0939F1ZV")  # Maharashtra
ka = iv.in_.gstin("29AAPFU0939F1ZB")  # Karnataka

assert 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
iv.in_.gstin("27AAPFU0939F1ZV")
iv.in_.gstin("27 aapfu0939f 1zv")
iv.in_.gstin("27-AAPFU0939F-1ZV")

Network failures in your code

Always wrap the API call in a try/except. A network timeout should not cause your invoice flow to crash — decide upfront whether to fail open or closed on API unavailability.

def validate_gstin_safe(gstin: str) -> dict:
    try:
        return validate_gstin(gstin)
    except requests.RequestException as exc:
        # Log and decide: fail open (allow) or fail closed (reject)
        logger.error("GSTIN validation failed: %s", exc)
        return {"valid": None, "error": "validation_unavailable"}

10. Summary

Do not rely on a basic regex for GSTIN — it misses state codes and check characters
Do not skip the mod-36 check character — it catches typos and transpositions
Validate the full structure: state code + PAN + entity number + check character
Use the parsed response to extract PAN, state, and entity type automatically
Handle multiple GSTINs per PAN across different states
Let the API normalize input — no need to strip spaces or uppercase manually

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.