A GST Identification Number looks like a random 15-character code, but every position in it means something. Once you can read one, you can catch most typing mistakes before you ever call an API — and you will understand why a GSTIN that “looks right” can still be wrong.
The 15 characters, position by position
- 1–2: State code. The two-digit code of the state or union territory where the business is registered — for example 07 for Delhi, 06 for Haryana, 27 for Maharashtra and 29 for Karnataka.
- 3–12: PAN. The ten-character PAN of the business or proprietor. This is why one PAN can be linked to several GSTINs across different states.
- 13: Entity number. How many registrations that PAN holds in the same state. It starts at 1, runs to 9, then continues with letters.
- 14: Default character. For regular registrations this is the letter Z. Some special registrations use a different character here.
- 15: Check character. Calculated from the first 14 characters. It exists to catch typing mistakes.
Take the commonly used example 27AAPFU0939F1ZV: 27 is Maharashtra, AAPFU0939F is the PAN, 1 means it is the first registration for that PAN in the state, Z is the default character and V is the check character.
Checking the format
The first test is the shape of the string. Two digits, five letters, four digits, a letter, then an entity character, one more character and the check character:
/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z][A-Z0-9][0-9A-Z]$/
Convert input to upper case and strip spaces before testing. People paste GSTINs from invoices, PDFs and WhatsApp messages, and stray spaces are the most common reason a correct number fails.
Checking the check character
The 15th character is calculated with a mod-36 checksum over the first 14 characters. Each character is mapped to a value (0–9 stay as they are, A is 10, B is 11, up to Z at 35). Every second character is multiplied by 2, the result is folded back into base 36, and the check character is whatever brings the total to a multiple of 36.
In PHP:
function gstin_is_valid( $gstin ) {
$gstin = strtoupper( preg_replace( '/s+/', '', $gstin ) );
if ( ! preg_match( '/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z][A-Z0-9][0-9A-Z]$/', $gstin ) ) {
return false;
}
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$sum = 0;
for ( $i = 0; $i < 14; $i++ ) {
$product = strpos( $chars, $gstin[ $i ] ) * ( $i % 2 ? 2 : 1 );
$sum += intdiv( $product, 36 ) + $product % 36;
}
return $chars[ ( 36 - $sum % 36 ) % 36 ] === $gstin[14];
}
This runs instantly, needs no network call and rejects most mistyped numbers — a swapped pair of characters or one wrong letter will almost always fail it.
A valid GSTIN is not a verified business
This is the part people miss. The checks above only prove the number is well-formed. They cannot tell you whether:
- the GSTIN was ever issued,
- the registration is still active, or has been cancelled or suspended,
- the legal name and address match the business you are dealing with.
For that you need the GST system itself. You can look a number up manually with the “Search Taxpayer” option on the official GST portal, or use a verification service that queries the registration data for you. I built GSTIN.cloud and GSTVerify for exactly this step.
Putting it together
A sensible order for any form or system that accepts GSTINs:
- Clean the input: trim, remove spaces, convert to upper case.
- Check the format and the check character locally, and show the user a clear message straight away if either fails.
- Only then look the number up to confirm the registration, its status and the legal name.
- Compare the state code with the address the user gave you. A mismatch is worth a second look.
Doing the cheap local checks first keeps your verification calls for numbers that can actually exist, and gives people instant feedback while they are still typing.