Data AnalyticsPractical workplace data tasks

Validate GSTIN and PAN formats, including the GSTIN check digit

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 6 min read
In this article (7 sections)

Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).

The short answer

A GSTIN is 15 characters: a two-digit state code, the holder's ten-character PAN, a two-character entity code and a final check character. To validate a column of them, upper-case and trim each value, confirm the state code, confirm the embedded PAN has the right pattern and a known status letter, recompute the check character and compare, and finally compare the embedded PAN with the PAN recorded for that vendor. Anything that fails goes to a correction list.

What the official sources say, and what they do not

Rule 10(1) of the CGST Rules sets the structure: two characters for the state code, ten for the PAN (or TAN), two for the entity code, and one checksum character. The two-digit state codes are listed in the master codes on the e-Invoice portal; Uttar Pradesh is 09, Delhi 07, Haryana 06 and Maharashtra 27, for example. The Income Tax Department explains that the fourth character of a PAN represents the status of the holder: C for a company, P for an individual, F for a firm or LLP, H for a HUF and so on.

Two things are commonly repeated but not stated in those sources, so this article does not enforce them. One is that the 14th character is always "Z"; treat the entity code as two free characters. The other is the exact check-character algorithm. The mod-36 method in the script below is the one widely used by accounting and validation tools, and it reproduced the final character of the real GSTIN we tested it against, but the authoritative test of any GSTIN is the search taxpayer facility on the GST portal.

Why a vendor master needs this at all

Vendor and customer masters are typed by people, often from a visiting card or an invoice photo. The same errors repeat: a letter O typed for a zero, lower-case letters, a stray space, a digit transposed. Each of these breaks a GSTR-2B match, an e-invoice upload or a TDS return later, and it is much cheaper to catch them when the master is built than at month-end.

The validation script

All vendor names, PANs and GSTINs below are synthetic. They were constructed so that the valid ones pass the format checks; they do not belong to any real business, so do not look them up.

python
import re
import pandas as pd

CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

# A subset of GST state codes, from the e-Invoice portal's master codes.
STATE_CODES = {"03": "Punjab", "06": "Haryana", "07": "Delhi", "08": "Rajasthan",
               "09": "Uttar Pradesh", "19": "West Bengal", "24": "Gujarat",
               "27": "Maharashtra", "29": "Karnataka", "33": "Tamil Nadu", "36": "Telangana"}

# Fourth character of PAN = status of the holder (Income Tax Department).
PAN_STATUS = {"A": "Association of Persons", "B": "Body of Individuals", "C": "Company",
              "F": "Firm/LLP", "G": "Government agency", "H": "HUF",
              "J": "Artificial juridical person", "L": "Local authority",
              "P": "Individual", "T": "Trust"}

PAN_RE = re.compile(r"[A-Z]{5}[0-9]{4}[A-Z]")

def gstin_check_char(first14: str) -> str:
    """Mod-36 check character used for GSTINs (weights alternate 1, 2)."""
    total = 0
    for i, ch in enumerate(first14):
        product = CHARS.index(ch) * (2 if i % 2 else 1)
        total += product // 36 + product % 36
    return CHARS[(36 - total % 36) % 36]

def check_pan(pan: str) -> str:
    if not PAN_RE.fullmatch(pan):
        return "bad PAN format"
    if pan[3] not in PAN_STATUS:
        return f"unknown PAN status letter {pan[3]}"
    return "ok"

def check_gstin(g: str, expected_pan: str | None = None) -> str:
    if len(g) != 15 or not re.fullmatch(r"[0-9A-Z]{15}", g):
        return "not 15 letters/digits"
    if g[:2] not in STATE_CODES:
        return f"state code {g[:2]} not in our list"
    pan_msg = check_pan(g[2:12])
    if pan_msg != "ok":
        return "embedded " + pan_msg
    if gstin_check_char(g[:14]) != g[14]:
        return f"check character should be {gstin_check_char(g[:14])}"
    if expected_pan and g[2:12] != expected_pan:
        return "PAN inside GSTIN differs from vendor master PAN"
    return "ok"

# Synthetic vendor master. None of these identifiers belongs to a real business.
vendors = pd.DataFrame({
    "vendor": ["Xylo Papers", "Zenith Tools", "R. Traders", "Yamuna Foods",
               "Westline Logistics", "Vega Chem", "Typo Supplies", "Old Record"],
    "pan": ["AAACX1234A", "AABCZ9012C", "AAGPR4821K", "AAFCY5678B",
            "AAGCW3456D", "AAHCV7890E", "AAHCT1111F", "aagcq22220"],
    "gstin": [" 09aaacx1234a1zv", "27AABCZ9012C1ZI", "29AAGPR4821K1ZA", "07AAFCY5678B1Z3",
              "06AAGCW3456D1ZE", "24AAHCV7890E1Z1", "45AAHCT1111F1ZK", "O9AAGCQ2222O1ZA"],
})

vendors["gstin_clean"] = vendors["gstin"].str.strip().str.upper()
vendors["pan_clean"] = vendors["pan"].str.strip().str.upper()
vendors["pan_check"] = vendors["pan_clean"].map(check_pan)
vendors["gstin_check"] = [check_gstin(g, p) for g, p in zip(vendors["gstin_clean"], vendors["pan_clean"])]
vendors["state"] = vendors["gstin_clean"].str[:2].map(STATE_CODES)
vendors["holder_type"] = vendors["pan_clean"].str[3].map(PAN_STATUS)
print(vendors[["vendor", "gstin_clean", "state", "holder_type", "pan_check", "gstin_check"]]
      .to_string(index=False))

Five rows pass. Three fail for three different reasons. Vega Chem's GSTIN ends in 1 where the check character works out to 0, which is the signature of a single mistyped character somewhere in the first fourteen. Typo Supplies has state code 45, which is not in the list. The old record contains the letter O in place of a zero twice, in the state code and at the end of the PAN.

How the check character works

Each of the first fourteen characters is converted to a number from 0 to 35 using the order 0–9 then A–Z. Characters in odd positions (counting from zero at the left) are multiplied by 2, the others by 1. Each product is split into its "tens and units" in base 36, product // 36 + product % 36, and the pieces are added up. The check character is whatever brings that total to the next multiple of 36. Because a single wrong character almost always changes the total, the method catches most one-character typing errors and many swaps of adjacent characters.

Checks that go beyond format

Embedded PAN against master PAN. The third to twelfth characters of a GSTIN are the holder's PAN. If the vendor master also stores a PAN, they should agree. A mismatch usually means the GSTIN belongs to a different entity in the same group, which matters for tax credits and for TDS.

State code against address. The first two digits should match the state in the vendor's billing address. A vendor with a Noida address and a GSTIN starting 07 may be billing from its Delhi registration, which is legitimate, but it affects whether a supply is intra-State or inter-State; see check whether an invoice should carry IGST or CGST and SGST.

One GSTIN, one vendor. Group the master by the cleaned GSTIN and list any that appear more than once. Duplicate vendors split spending, and they are often where duplicate payments come from.

An Excel version for quick checks

A formula can do the format part without macros. With the GSTIN in A2:

text
=AND(LEN(TRIM(A2))=15,
     ISNUMBER(--LEFT(A2,2)),
     ISNUMBER(--MID(A2,8,4)),
     SUMPRODUCT(--ISNUMBER(--MID(A2,{3,4,5,6,7},1)))=0,
     ISERROR(--MID(A2,12,1)))

It checks length, the numeric state code, the four digits inside the PAN and the letters around them. It does not check the check character; for that, a Power Query custom function or the Python script is easier to maintain than an array formula nobody else can read.

Keep the original value

Store the cleaned GSTIN in a separate column and keep what was typed. When a supplier disputes a mismatch, you need to show the value on record. Re-run the check whenever the master is edited, and add it to the onboarding step so new vendors are checked before their first invoice.

This validation feeds straight into reconciling GSTR-2B with your purchase register. Data quality checks on Indian business identifiers like these are part of the practical Python and Excel work in our Data Analyst course.

Frequently asked questions

Does a valid GSTIN format mean the registration is active?

No. A format check only catches typing mistakes. Whether a GSTIN is registered, active or cancelled is shown by the search taxpayer facility on the GST portal, and that is the check to use before relying on it for a claim or an invoice.

Can I verify the last character of a PAN the same way?

The Income Tax Department describes the tenth character as an alphabetic check digit, but it does not publish the calculation. You can check the pattern of letters and digits, and the fourth-character status code, but not the check digit itself.

Why is the 13th character of my GSTIN a digit?

The rules call characters 13 and 14 the entity code. Businesses with more than one registration in a state against the same PAN get different entity codes, so do not assume a fixed value in your validation.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.

Explore Data Analytics with Generative AI
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.