The GSTIN Regex — and Why Most Published Ones Are Wrong
The correct regular expression for an Indian GST number, the two mistakes almost every copy-pasted version makes, and what a regex can never check.
Here is the pattern, before anything else:
^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$
Most published GSTIN regexes differ from that one in at least one place, and each difference quietly accepts strings the government could never have issued. This article walks the pattern group by group, names the two mistakes that account for nearly all of them, and is honest about the boundary: there is a whole class of wrong GSTIN that no regular expression will ever catch.
The regex, and what each group means
A GSTIN is exactly fifteen characters drawn from 0-9 and A-Z, and every one of them
has a job. Take 27AAACR5055K1Z7 and line it up against the pattern:
^ [0-9]{2} [A-Z]{5}[0-9]{4}[A-Z] [1-9A-Z] Z [0-9A-Z] $
27 AAACR5055K 1 Z 7
│ │ │ │ └─ check digit
│ │ │ └─── reserved, a literal 'Z'
│ │ └──────────── entity code, starts at 1
│ └────────────────────────────────── the PAN of the registered entity
└─────────────────────────────────────────── state code
| Group | Position | What it holds |
|---|---|---|
[0-9]{2} | 1–2 | State code. 01–38 plus 96, 97 and 99. Not every value in that range is issued, and a regex cannot usefully express which — see the GST state code list |
[A-Z]{5}[0-9]{4}[A-Z] | 3–12 | The embedded PAN: five letters, four digits, one letter |
[1-9A-Z] | 13 | Entity code — the nth registration for this PAN in this state. Never 0 |
Z | 14 | Reserved. A literal Z in every GSTIN issued under the current scheme |
[0-9A-Z] | 15 | The check digit. It really can be 0, unlike the entity code |
The PAN block carries more meaning than the pattern shows. Its fourth character encodes entity type —
C company, P individual or proprietor, F firm or LLP, H
HUF, T trust, A AOP, B BOI, G government, L
local authority, J artificial juridical person. So AAACR5055K has C in
the fourth slot and belongs to a company. A regex can enumerate those ten letters if you want, but doing so
buys you very little and breaks the day a new code appears; the full field-by-field reading is in
GSTIN format explained.
Mistake 1: allowing a zero entity code
This is the most widely copied error, and it hides in plain sight because it looks symmetrical:
// Wrong — accepts an entity code of 0.
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/
// Correct.
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/
The thirteenth character counts registrations: it is the first, second, third registration for this PAN in
this state, and counting starts at 1. There is no zeroth registration, so
0 is never issued in that position. A pattern with [0-9A-Z] there accepts
27AAACR5055K0Z7, which is structurally impossible and should have been rejected before it ever
reached your database or your API budget.
Mistake 2: lumping the last three characters together
The second mistake is a tidiness instinct. Three trailing alphanumerics, so why not write them as one group?
// Wrong — the literal Z has vanished.
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}$/
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][A-Z0-9]{3}$/
// Correct — three separate positions, because they mean three different things.
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/
Collapsing positions 13–15 throws away the cheapest structural check in the entire string. Position 14 is
fixed. It is not a range, not a class, not a probability — it is the letter Z in every
GSTIN issued under the current scheme. A number without Z there is malformed whatever else is
true about it, and a single literal character in your pattern catches it for nothing.
27AAACR5055K1Y7 is the test case. Against the collapsed pattern it passes. Against the correct
one it fails immediately, which is exactly what you want, because that string cannot be a GST number.
Patterns of this shape also usually reintroduce Mistake 1 at the same time, since [0-9A-Z]{3}
permits a zero entity code as a side effect.
Anchors, case, and the \d trap
Missing anchors
Without ^ and $ the pattern matches a GSTIN embedded in a longer string,
so "my gstin is 27AAACR5055K1Z7 ok" validates cleanly and you store the sentence. In JavaScript
this bites through String.match and RegExp.test; in Python it bites through
re.search. Python's re.match is a half-measure — it anchors the start only, so you
still need a trailing $, or better, re.fullmatch.
The anchors earn their keep twice over: they also fix the length at fifteen. An anchored pattern of fixed-width groups cannot match a fourteen- or sixteen-character string, so you never need a separate length check.
Case, and why the i flag is a trap
The pattern is uppercase-only by design. It is tempting to reach for the i flag so that users
who type in lowercase are not punished — but that flag accepts 27aaacr5055k1z7
as given, and the lowercase value then flows straight into your database. Two rows for the same
registration is a real cost: reconciliation misses, duplicate vendors, joins that silently return nothing.
Normalise first, then match the strict pattern:
// Normalise, then validate. Never the i flag.
const PATTERN = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
export function normalise(value) {
return String(value ?? '').toUpperCase().replace(/[^0-9A-Z]/g, '');
}
export function matchesGstinFormat(value) {
return PATTERN.test(normalise(value));
}
matchesGstinFormat('27aaacr5055k1z7'); // true — normalised first
matchesGstinFormat(' 27-AAACR-5055K1Z7 '); // true — spaces and dashes stripped
PATTERN.test('27aaacr5055k1z7'); // false — and it should be
The important part is that normalise returns the value you go on to store. Validate the
normalised string and persist the same one, and the whole class of case and punctuation duplicates
disappears. The input-level behaviour that stops the problem earlier still — uppercasing as the user types —
is covered in GST number validation in web forms.
The \d trap
\d is not always [0-9]. In .NET, and in Python 3's re module with a
str pattern, \d matches any Unicode decimal digit — Devanagari
०१२३, Arabic-Indic digits, and dozens of other scripts. A GSTIN permits exactly ten ASCII
digits, so [0-9] is both correct and unambiguous in every engine.
What a regex can never check
The fifteenth character is not data. It is a check digit, computed arithmetically from the first fourteen using a modulus-36 variant of the Luhn algorithm. No regular expression can compute it, because regular languages cannot do arithmetic over their own input. That is not a limitation of your pattern; it is a limitation of the class of pattern.
So the decisive test case is 27AAACR5055K1Z0. It matches the correct pattern in every
particular — right state code shape, right PAN shape, entity code 1, literal Z,
a legal check-digit character. And it is impossible: the arithmetic says that string's final character must
be 7. A regex will never tell you that. The algorithm itself, and a measurement of exactly what
it catches, is in the GSTIN checksum algorithm.
| Question | Answered by |
|---|---|
| Is this the right shape? | The regex. Free, instant, catches typos of shape |
| Is this arithmetically possible? | The checksum. Also offline, also free, but not expressible as a pattern |
| Does this registration exist, and is it active? | Only a lookup against the government register |
Which is the correct posture in three lines of code: pattern, then checksum, then — for the numbers that actually matter — a verification call. The distinction between the second and third stages is the subject of validation vs verification.
The same pattern in nine places
The pattern is identical everywhere; only the syntax around it changes. Note the absence of \d
in all nine.
| Where | How to write it |
|---|---|
| JavaScript / TypeScript | const GSTIN = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;then GSTIN.test(value) |
| Python | re.fullmatch(r"[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]", value) — fullmatch anchors both ends for you |
| PHP | preg_match('/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/', $value) |
| Java | Pattern.compile("^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$"), then matcher(value).matches() |
| C# | new Regex(@"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$", RegexOptions.None) — and this is the engine where \d would have matched Unicode digits |
| Go | var gstin = regexp.MustCompile(`^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$`) |
| PostgreSQL | gstin ~ '^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$' — usable as a CHECK constraint |
| MySQL 8 | REGEXP_LIKE(gstin, '^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$', 'c') — the 'c' forces case-sensitive matching |
| Excel / Google Sheets | Excel formulas have no regex; use Power Query or a helper column. Google Sheets does: =REGEXMATCH(A2, "^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$") |
Test strings that break bad patterns
Every row below is chosen to fail a specific wrong pattern. If your implementation agrees with all eleven, it is right.
| Input | Pattern | Why it is on the list |
|---|---|---|
27AAACR5055K1Z7 | Match | Valid — Reliance, Maharashtra. Checksum passes too |
29AAACI1195H1ZI | Match | Valid, and the check digit is a letter — trips patterns that end [0-9]$ |
99AAACR5055KAZN | Match | Valid. Entity code A, state code 99 |
27AAACR5055K1Z0 | Match | Pattern passes, checksum fails. The case that proves regex is not enough |
27AAACR5055K0Z7 | No match | Entity code 0. Catches Mistake 1 |
27AAACR5055K1Y7 | No match | Position 14 is not Z. Catches Mistake 2 |
27AAACR50X5K1Z7 | No match | Letter inside the PAN's digit block |
271234R5055K1Z7 | No match | Digits inside the PAN's letter block |
my gstin is 27AAACR5055K1Z7 | No match | Catches missing anchors |
27aaacr5055k1z7 | No match as given | Passes after normalising. Catches a careless i flag |
००AAACR5055K1Z7 | No match | Devanagari digits. Catches the \d trap |
Two rows deserve a second look. 29AAACI1195H1ZI ends in a letter, which is perfectly legal and
which any pattern ending [0-9]$ rejects — a surprisingly common variant, written by someone who
saw a hundred sample GSTINs and never hit a letter. And 27AAACR5055K1Z0 is the row that should
change how you use the pattern at all: it is shape-perfect and arithmetically impossible, so any code path
that treats "matched the regex" as "valid GSTIN" is making a claim it has not earned.
Our open-source libraries ship this exact pattern alongside the checksum, MIT-licensed and dependency-free:
npm install @dlminds/gstin-api
pip install gstin-toolkit
| Node / TypeScript | Python | Answers |
|---|---|---|
matchesGstinFormat | matches_gstin_format | The pattern alone |
isValidGstin | is_valid_gstin | Pattern and checksum |
gstinRejectionReason | gstin_rejection_reason | A human-readable reason string, for the error message under the field |
The packages are on npm and PyPI, with the language-specific walkthroughs in Node.js and TypeScript and Python. For one-off numbers the free GSTIN validator runs entirely in your browser, the GST number search checks the live register, and the API documentation covers the verification endpoint you reach for once the pattern and the checksum have both said yes.
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.