DEV Community

Tarun Vaghasia
Tarun Vaghasia

Posted on

How GSTIN Checksum Validation Works (and Why It Isn't Enough)

Every Indian GST invoice carries a 15-character GSTIN. If you've ever had to validate one in code, you've probably written a regex like this and called it done:

/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/
Enter fullscreen mode Exit fullscreen mode

That regex checks the shape of a GSTIN. It says nothing about whether the number is real. A typo that happens to land on a syntactically valid string will sail straight through it — and if that typo ends up on an invoice, it's the kind of thing that gets flagged months later during a GST audit.

There's a cheap next step before you go anywhere near a network call: the GSTIN has a built-in checksum character, and you can verify it in a few lines with no API and no dependency.

How the checksum works

A GSTIN is 15 characters: 33AAACC1206D1ZN. The first 14 encode the state code, the business's PAN, and an entity number. The 15th is a checksum digit computed from the other 14, using a 36-character alphabet (0-9 then A-Z) and alternating weights of 1 and 2 — the same family of algorithm as ISO 7064 MOD 37-36.

const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

function gstinChecksumIsValid(gstin) {
  if (!/^[0-9A-Z]{15}$/.test(gstin)) return false;

  let sum = 0;
  let factor = 1;
  for (let i = 0; i < 14; i++) {
    const codePoint = ALPHABET.indexOf(gstin[i]);
    const digit = factor * codePoint;
    sum += Math.floor(digit / 36) + (digit % 36);
    factor = factor === 1 ? 2 : 1;
  }
  const checksumIndex = (36 - (sum % 36)) % 36;
  return ALPHABET[checksumIndex] === gstin[14];
}

gstinChecksumIsValid('33AAACC1206D1ZN'); // true
gstinChecksumIsValid('33AAACC1206D1ZM'); // false — last char changed
Enter fullscreen mode Exit fullscreen mode

I hand-verified this against a real, currently-registered GSTIN (Central Warehousing Corporation, 33AAACC1206D1ZN) before trusting it enough to write this post — the algorithm above reproduces the N checksum character exactly.

Python, same logic:

ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def gstin_checksum_is_valid(gstin: str) -> bool:
    import re
    if not re.match(r'^[0-9A-Z]{15}$', gstin):
        return False

    total = 0
    factor = 1
    for ch in gstin[:14]:
        code_point = ALPHABET.index(ch)
        digit = factor * code_point
        total += digit // 36 + digit % 36
        factor = 2 if factor == 1 else 1

    checksum_index = (36 - total % 36) % 36
    return ALPHABET[checksum_index] == gstin[14]
Enter fullscreen mode Exit fullscreen mode

Drop this in ahead of any network call. It rejects a huge class of typos and malformed input for free, in constant time, offline.

What a checksum can't tell you

A checksum only proves the string is internally consistent — that it could plausibly be a GSTIN. It doesn't tell you:

  • whether that GSTIN is actually registered
  • whether the registration is still active, or was cancelled
  • what legal name it's registered under, so you can catch a mismatch against the invoice

You can construct a string that's checksum-valid and still refers to nobody, or to a business whose registration was cancelled last year. For that you need to ask the actual GST network, which means an API call.

That's the part I build: gstinapi.in is a REST API that takes a GSTIN and returns the legal name, registration status, taxpayer type and registered address, live from India's official GSP network — no scraping, no cached copy.

const { GstinApi } = require('gstinapi'); // npm install gstinapi

const client = new GstinApi({ apiKey: process.env.GSTIN_API_KEY });
const result = await client.lookup('33AAACC1206D1ZN');

console.log(result.data.legal_name); // CENTRAL WAREHOUSING CORPORATION
console.log(result.data.status);     // Active
Enter fullscreen mode Exit fullscreen mode
from gstinapi import GstinApi  # pip install gstinapi

client = GstinApi(api_key=os.environ["GSTIN_API_KEY"])
result = client.lookup("33AAACC1206D1ZN")

print(result["data"]["legal_name"])  # CENTRAL WAREHOUSING CORPORATION
print(result["data"]["status"])      # Active
Enter fullscreen mode Exit fullscreen mode

Every account gets 100 free lookups a month, no credit card, and it renews every month rather than being a one-time trial credit — plenty to validate the idea before you need volume.

Source for the client libraries: gstinapi-node and gstinapi-python. Full API reference is at gstinapi.in/docs.

If you build GSTIN validation into something, I'd like to hear where the checksum check alone was enough and where you actually needed the live lookup — drop it in the comments.

Top comments (0)