Deep dive 11 min read

The GSTIN Check Digit Algorithm, Measured

How the modulus-36 checksum in a GST number works, with a worked example — and exhaustive measurements of which typos it catches and the one it misses.

Written and reviewed by the GSTIN API team Last reviewed

The last character of a GST number is not part of the identifier. It is arithmetic — a check digit computed from the fourteen characters in front of it, so that a mistyped GSTIN can be caught without asking anybody.

The algorithm is public and widely restated. What is almost never written down is what it is worth: which errors it catches, which it does not, and where the boundary sits. This article covers both, and the second half is measured by exhaustive search rather than asserted.

What the fifteen characters hold

A GSTIN is exactly fifteen characters drawn from 0-9 and A-Z. Take 27AAACR5055K1Z7:

27  AAACR5055K  1  Z  7
│   │           │  │  └─ check digit
│   │           │  └──── reserved, always 'Z'
│   │           └─────── which registration this is for this PAN in this state
│   └─────────────────── the PAN of the registered entity
└─────────────────────── state code
PositionContentsAllowed characters
1–2State codeTwo digits. 01–38 plus 96, 97, 99 — and not every value in that range is issued
3–12PAN of the registered entity5 letters, 4 digits, 1 letter
13Entity code — the nth registration for this PAN in this state1–9 then A–Z. Never 0
14ReservedA literal Z
15Check digitAny of the 36 characters, 0 included
Position 13 never holds a zero. Position 15 can.

The full field-by-field meaning, including what the fourth character of the embedded PAN tells you about the entity type, is in GSTIN format explained.

The modulus-36 algorithm

If you know the Luhn algorithm from credit cards, this is the same shape with a wider alphabet. Each character's numeric value is its index in:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ

So 0 is 0, 9 is 9, A is 10, and Z is 35. Then:

  1. Walk the first fourteen characters left to right, multiplying each character's value by an alternating weight — 1, 2, 1, 2 … starting at 1.
  2. Fold each product back into a single value: floor(product / 36) + (product % 36). This is the base-36 equivalent of "add the digits" when base-10 Luhn doubles a digit past 9.
  3. Sum all fourteen folded values.
  4. The check digit is the character at index (36 - (sum % 36)) % 36.

A worked example, character by character

Here is 27AAACR5055K1Z — the first fourteen characters of Reliance Industries' Maharashtra GSTIN:

#CharValueWeightProductFolded
122122
27721414
3A1011010
4A1022020
5A1011010
6C1222424
7R2712727
85521010
900100
105521010
1155155
12K202405
1311111
14Z3527035
Sum = 173. 173 mod 36 = 29. 36 − 29 = 7. Index 7 is the character '7'.

Rows 12 and 14 are where the folding shows. 20 × 2 = 40, and 40 over 36 is 1 remainder 4, so 1 + 4 = 5. Likewise 35 × 2 = 70 folds to 1 + 34 = 35. Every other row has a product under 36 and folds to itself.

The sum is 173. 173 mod 36 = 29, and 36 − 29 = 7. The character at index 7 is 7 — and the real GSTIN is indeed 27AAACR5055K1Z7.

What the checksum actually catches

Everyone restates the algorithm. Almost nobody says what it is worth, so we measured it by exhaustive search over the base string 27AAACR5055K1Z:

from itertools import permutations
from gstin_api import gstin_check_digit

CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
base = "27AAACR5055K1Z"

# Every single-character substitution: 14 positions x 35 other characters.
missed_substitutions = sum(
    gstin_check_digit(base[:i] + c + base[i + 1:]) == gstin_check_digit(base)
    for i in range(14)
    for c in CHARSET
    if c != base[i]
)

# Every adjacent transposition.
missed_transpositions = {
    a + b
    for i in range(13)
    for a, b in permutations(CHARSET, 2)
    if gstin_check_digit(base[:i] + a + b + base[i + 2:])
    == gstin_check_digit(base[:i] + b + a + base[i + 2:])
}

print(missed_substitutions)    # 0, out of 490
print(missed_transpositions)   # {'0Z', 'Z0'}

Every single-character typo is caught

0 out of 490 single-character substitutions produce the same check digit. Mistype any one character anywhere in the first fourteen and the checksum notices. That is the strong guarantee, and it is the whole reason offline validation earns its place: single-character slips are by far the most common data-entry error, and they are caught for free, in microseconds, with no network call.

The one transposition it misses

Swap two adjacent characters and the checksum notices — unless the two characters are 0 and Z. Those two, and only those two, in either order.

The reason falls out of the folding step. Write f(x) = floor(x/36) + x%36. Then:

  • For a character value v < 18, doubling gives 2v < 36, which folds to itself: f(2v) = 2v.
  • For v ≥ 18, doubling gives 2v ≥ 36, which folds to 2v − 35.

Transposing values a and b across a weight-1 / weight-2 boundary leaves the sum unchanged only when a + f(2b) = b + f(2a). Work through the three cases and every one collapses to a = b, except the mixed case where a < 18 ≤ b. There the condition becomes b = a + 35, and across a 36-character alphabet the only solution is a = 0, b = 35 — that is, 0 and Z.

And a one-in-36 chance for anything invented

If someone invents a fifteen-character string that happens to satisfy the format pattern, it has roughly a 2.8% chance of also satisfying the checksum. The check digit turns "looks like a GSTIN" into "is 36 times less likely to be fabricated". That is a real improvement and it is not proof.

What it can never catch

This is the important part, and it is not a limitation of the algorithm. It is a limitation of arithmetic.

SituationChecksum saysReality
Registration was cancelled last quarterValidThe first fourteen characters did not change, so the check digit still agrees. Cancelled and suspended registrations keep their checksum forever
Number was never issuedValid00AAACR5055K1ZN has a correct check digit. 00 is not a state code the government issues
GSTIN belongs to a different companyValidOnly the register knows whose name is on it
Number was fabricated deliberatelyValidThe algorithm is public. This article is part of the reason it is public
Every row here is invisible to any offline check, in any language.

Which gives the correct posture — a two-stage funnel, covered in full in validation vs verification:

  1. Validate offline. Free, instant, catches every typo, and costs nothing to run across a million rows.
  2. Verify against the register for whatever survives and actually matters — a new supplier, an invoice you are about to pay, a GSTIN going onto a return.

Stage one exists to make stage two cheap. It is not a substitute for it.

An implementation, and how to test yours

The algorithm is about twenty lines in any language. Our open-source implementations are on PyPI and npm, both MIT-licensed and dependency-free — see the client libraries for what each one exports:

CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"


def gstin_check_digit(first14: str) -> str:
    total = 0

    for index, character in enumerate(first14[:14]):
        product = CHARSET.index(character) * (1 if index % 2 == 0 else 2)
        total += product // 36 + product % 36

    return CHARSET[(36 - total % 36) % 36]


gstin_check_digit("27AAACR5055K1Z")   # '7'
gstin_check_digit("29AAACI1195H1Z")   # 'I'  <- letters happen, handle them
gstin_check_digit("07AABCU9603R1Z")   # 'P'

If you would rather write your own — and this article exists partly so you can — test it against these. They are chosen to break the implementations that look right:

InputValid?Why it is on the list
27AAACR5055K1Z7YesA real, live registration
29AAACI1195H1ZIYesCheck digit is a letter
99AAACR5055KAZNYesEntity code A, state code 99
00AAACR5055K1ZNYesValid checksum, state code never issued
27AAACR5055K1Z0NoCheck digit off by one
27AAACR5055K1Y7NoPosition 14 is not Z
27AAACR5055K0Z7NoEntity code 0, which is never issued
27AAACR50X5K1Z7NoLetter inside the PAN's digit block
27aaacr5055k1z7YesLowercase must normalise, not fail
27-AAACR-5055K1Z7YesSo must the punctuation people paste in
The row that separates a careful implementation from a hasty one is 00AAACR5055K1ZN.

00AAACR5055K1ZN is the one that matters. Its checksum is genuinely correct, so a validator that reports "invalid" is making a different claim from the one it thinks it is making. The honest answer is two separate facts: structurally valid, and state code 00 is not one the government issues. Collapsing them into a single boolean is how a validator ends up disagreeing with the API sitting behind it.

From here: the language-specific walkthroughs are validating a GSTIN in Python and in Node.js and TypeScript, the pattern itself is picked apart in the GSTIN regex, and if you just want an answer for one number, the free GSTIN validator runs entirely in your browser.

Frequently asked questions

It is the fifteenth and last character, computed arithmetically from the first fourteen using a modulus-36 variant of the Luhn algorithm. Its job is to make typos self-detecting: change any single character of a GSTIN and the check digit no longer agrees.
It is the same shape but base 36 rather than base 10, because a GSTIN contains letters as well as digits. Each character takes its index in the alphabet 0-9 then A-Z, weights alternate 1 and 2, and each product is folded by adding its quotient and remainder over 36 rather than by adding decimal digits.
You can generate a string that passes validation, and that is exactly why validation alone proves very little. The algorithm is public, so a checksum-correct GSTIN is trivial to fabricate. Only a lookup against the government register shows whether a number was actually issued and is still active.
No. The check digit is a function of the first fourteen characters, and those do not change when a business deregisters. A cancelled GSTIN keeps its correct checksum forever.
It is reserved for future use and has been a literal Z in every GSTIN issued under the current scheme. Because it is fixed, it doubles as a cheap sanity check: a number without Z in position 14 is malformed regardless of what the checksum says.
Roughly one in 36, or about 2.8%, for a random string that already satisfies the format pattern. So the check digit makes a fabricated number 36 times less likely to slip through — useful, and nowhere near proof.

A note on accuracy. GST rules change often. This article reflects our understanding as of 8 September 2026 and is general information, not tax or legal advice. For the authoritative position, check gst.gov.in and cbic-gst.gov.in, or speak to a qualified tax professional about your specific situation.

Check a GSTIN right now, free

Five lookups a day, no account required. Status, filing history, e-invoicing and jurisdiction.

Free GST search tool

Build it into your product

One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.

Start free