Reference 8 min read

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.

Written and reviewed by the GSTIN API team Last reviewed

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
GroupPositionWhat it holds
[0-9]{2}1–2State 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–12The embedded PAN: five letters, four digits, one letter
[1-9A-Z]13Entity code — the nth registration for this PAN in this state. Never 0
Z14Reserved. A literal Z in every GSTIN issued under the current scheme
[0-9A-Z]15The check digit. It really can be 0, unlike the entity code
Five groups, and only one of them is a free-for-all.

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.

QuestionAnswered 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
Three questions, and only the first has a regex-shaped answer.

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.

WhereHow to write it
JavaScript / TypeScriptconst GSTIN = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/;
then GSTIN.test(value)
Pythonre.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
PHPpreg_match('/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/', $value)
JavaPattern.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
Govar gstin = regexp.MustCompile(`^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$`)
PostgreSQLgstin ~ '^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$' — usable as a CHECK constraint
MySQL 8REGEXP_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 SheetsExcel 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]$")
One pattern, nine syntaxes. Excel formulas are the only entry with no regex at all.

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.

InputPatternWhy it is on the list
27AAACR5055K1Z7MatchValid — Reliance, Maharashtra. Checksum passes too
29AAACI1195H1ZIMatchValid, and the check digit is a letter — trips patterns that end [0-9]$
99AAACR5055KAZNMatchValid. Entity code A, state code 99
27AAACR5055K1Z0MatchPattern passes, checksum fails. The case that proves regex is not enough
27AAACR5055K0Z7No matchEntity code 0. Catches Mistake 1
27AAACR5055K1Y7No matchPosition 14 is not Z. Catches Mistake 2
27AAACR50X5K1Z7No matchLetter inside the PAN's digit block
271234R5055K1Z7No matchDigits inside the PAN's letter block
my gstin is 27AAACR5055K1Z7No matchCatches missing anchors
27aaacr5055k1z7No match as givenPasses after normalising. Catches a careless i flag
००AAACR5055K1Z7No matchDevanagari digits. Catches the \d trap
Row four is the one that proves a regex alone is never enough.

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 / TypeScriptPythonAnswers
matchesGstinFormatmatches_gstin_formatThe pattern alone
isValidGstinis_valid_gstinPattern and checksum
gstinRejectionReasongstin_rejection_reasonA human-readable reason string, for the error message under the field
Three functions, because there are three different questions.

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

^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$ — two digits for the state code, a ten-character PAN, an entity code that starts at 1 and never at 0, a literal Z, and a check digit that may be any of the 36 characters. Anchor it at both ends and match uppercase only.
The thirteenth character counts registrations for a given PAN within a state, and that counting starts at 1. A zero there is never issued, so 27AAACR5055K0Z7 should be rejected on format alone. Patterns that write [0-9A-Z] accept it, and they are the most widely copy-pasted patterns on the internet.
No. The fifteenth character is arithmetic over the first fourteen — a modulus-36 Luhn variant — and no regular expression can compute it. A pattern match is necessary but not sufficient: 27AAACR5055K1Z0 matches the pattern perfectly and is still an impossible number.
No. Add the i flag and a lowercase value such as 27aaacr5055k1z7 passes validation and lands in your database as typed, which is how you end up with two rows for one registration. Normalise first — uppercase and strip everything that is not a letter or digit — then match the uppercase-only pattern.
Because \d is not always [0-9]. In .NET, and in Python 3 re with a str pattern, \d matches any Unicode decimal digit, including Devanagari and Arabic-Indic digits. [0-9] means exactly the ten ASCII digits in every engine, which is what a GSTIN actually permits.
No. A regex describes shape, nothing more. It cannot tell you whether the number was ever issued, who it belongs to, or whether the registration is still active — only a lookup against the government register answers those.

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