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.
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
| Position | Contents | Allowed characters |
|---|---|---|
| 1–2 | State code | Two digits. 01–38 plus 96, 97, 99 — and not every value in that range is issued |
| 3–12 | PAN of the registered entity | 5 letters, 4 digits, 1 letter |
| 13 | Entity code — the nth registration for this PAN in this state | 1–9 then A–Z. Never 0 |
| 14 | Reserved | A literal Z |
| 15 | Check digit | Any of the 36 characters, 0 included |
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:
- Walk the first fourteen characters left to right, multiplying each character's value by an alternating weight — 1, 2, 1, 2 … starting at 1.
- 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. - Sum all fourteen folded values.
- 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:
| # | Char | Value | Weight | Product | Folded |
|---|---|---|---|---|---|
| 1 | 2 | 2 | 1 | 2 | 2 |
| 2 | 7 | 7 | 2 | 14 | 14 |
| 3 | A | 10 | 1 | 10 | 10 |
| 4 | A | 10 | 2 | 20 | 20 |
| 5 | A | 10 | 1 | 10 | 10 |
| 6 | C | 12 | 2 | 24 | 24 |
| 7 | R | 27 | 1 | 27 | 27 |
| 8 | 5 | 5 | 2 | 10 | 10 |
| 9 | 0 | 0 | 1 | 0 | 0 |
| 10 | 5 | 5 | 2 | 10 | 10 |
| 11 | 5 | 5 | 1 | 5 | 5 |
| 12 | K | 20 | 2 | 40 | 5 |
| 13 | 1 | 1 | 1 | 1 | 1 |
| 14 | Z | 35 | 2 | 70 | 35 |
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 gives2v < 36, which folds to itself:f(2v) = 2v. - For
v ≥ 18, doubling gives2v ≥ 36, which folds to2v − 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.
| Situation | Checksum says | Reality |
|---|---|---|
| Registration was cancelled last quarter | Valid | The first fourteen characters did not change, so the check digit still agrees. Cancelled and suspended registrations keep their checksum forever |
| Number was never issued | Valid | 00AAACR5055K1ZN has a correct check digit. 00 is not a state code the government issues |
| GSTIN belongs to a different company | Valid | Only the register knows whose name is on it |
| Number was fabricated deliberately | Valid | The algorithm is public. This article is part of the reason it is public |
Which gives the correct posture — a two-stage funnel, covered in full in validation vs verification:
- Validate offline. Free, instant, catches every typo, and costs nothing to run across a million rows.
- 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:
| Input | Valid? | Why it is on the list |
|---|---|---|
27AAACR5055K1Z7 | Yes | A real, live registration |
29AAACI1195H1ZI | Yes | Check digit is a letter |
99AAACR5055KAZN | Yes | Entity code A, state code 99 |
00AAACR5055K1ZN | Yes | Valid checksum, state code never issued |
27AAACR5055K1Z0 | No | Check digit off by one |
27AAACR5055K1Y7 | No | Position 14 is not Z |
27AAACR5055K0Z7 | No | Entity code 0, which is never issued |
27AAACR50X5K1Z7 | No | Letter inside the PAN's digit block |
27aaacr5055k1z7 | Yes | Lowercase must normalise, not fail |
27-AAACR-5055K1Z7 | Yes | So must the punctuation people paste in |
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
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 toolBuild it into your product
One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.
Start freeMore on developer integration guides
Browse every guide in the GST API blog.