How-to 8 min read

Test GST Numbers: Generate Valid GSTINs for Your Test Suite

Why hardcoded dummy GST numbers break your tests, how to generate checksum-correct GSTINs on demand, and the line you must not cross.

Written and reviewed by the GSTIN API team Last reviewed

You are writing tests for something that takes a GST number — an onboarding form, an invoice importer, a vendor master — and you need values to feed it. The obvious move is to search for a list of dummy GSTINs. There isn't one — and what you actually want is better: a generator, a corpus of edge cases, and a clear sense of where the boundary sits between test data and a document you must never produce. All three are below.

There is no official list of test GSTINs

Say it plainly, because most people arrive here expecting otherwise. GSTN operates no public developer portal, publishes no sandbox credentials, and has never reserved a range of GSTINs for testing the way card networks reserve 4242 4242 4242 4242. There is no 00TEST0000T0Z0 waiting for you. The reason is structural: GSTN's own APIs go only to licensed GST Suvidha Providers under a commercial agreement, so there is no self-serve tier to hang a sandbox off — the whole story is in is there a free government GST API.

Why a hardcoded dummy number rots

The tempting shortcut is to search for a well-known company, copy its GSTIN off a Google result or an invoice PDF, and paste it into a fixture file. It works on the day you do it.

Then it rots. That number belongs to a live registration whose status field is maintained by the GST department, not by you. Registrations get cancelled. They get suspended for non-filing. Businesses restructure and surrender a state registration. Your test asserting result.status == 'Active' is, in effect, a monitoring check on a company you have no relationship with — and it will go red in CI on a Tuesday morning during an unrelated deploy, and you will spend forty minutes finding out why.

ApproachPasses validation?Failure mode
Typed by hand, e.g. 27ABCDE1234F1Z5Usually notThe check digit is almost certainly wrong, so the fixture silently exercises only your rejection path
A real company's GSTINYesIts live status can change at any time; you have coupled your suite to a third party's compliance
Generated from a PAN and state codeYes, by constructionNone, provided it never leaves the test environment
Three sources of test GSTINs, and what each one costs you.

The first row is worth dwelling on, because it is the quiet one. A made-up number has roughly a 1 in 36 chance of accidentally having a correct check digit. So a hand-typed fixture is about 97% likely to be rejected by your own validator — which means a test called test_accepts_valid_gstin can sit green for a year while never once exercising the accept path. We measured that probability in the GSTIN checksum algorithm.

Generate one from a PAN and a state code

A GSTIN is not random. It is assembled, and the last character is arithmetic:

^[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, computed from the 14 before it
│   │           │  └──── reserved, always a literal 'Z'
│   │           └─────── entity code: '1'-'9' then 'A'-'Z', never '0'
│   └─────────────────── the 10-character PAN
└─────────────────────── 2-digit state code

So to build one you choose the first fourteen characters and compute the fifteenth. The algorithm is a modulus-36 Luhn variant: values are indices into 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ, weights alternate 1 and 2 from index 0, each product is folded as floor(p / 36) + p % 36, and the check digit is the character at index (36 - sum % 36) % 36. The derivation and a worked example are in the checksum article; the pattern itself is picked apart in the GSTIN regex explained. You do not have to implement any of it — our libraries are MIT-licensed, dependency-free, and expose the constructor directly:

# pip install gstin-toolkit
from gstin_api import build_gstin, is_valid_gstin, parse_gstin

build_gstin("AAACR5055K", "27")                      # '27AAACR5055K1Z7'
build_gstin("AAACR5055K", "29", entity_code="2")     # second registration in Karnataka
build_gstin("AAACI1195H", "29")                      # '29AAACI1195H1ZI' - letter check digit

is_valid_gstin("27AAACR5055K1Z7")                    # True
parse_gstin("27AAACR5055K1Z7").state_code            # '27'

build_gstin("NOTAPAN", "27")                         # raises ValueError
build_gstin("AAACR5055K", "27", entity_code="0")     # raises ValueError
// npm install @dlminds/gstin-api
import { buildGstin, isValidGstin, gstinCheckDigit } from '@dlminds/gstin-api';

buildGstin({ pan: 'AAACR5055K', stateCode: '27' });                    // '27AAACR5055K1Z7'
buildGstin({ pan: 'AAACR5055K', stateCode: '29', entityCode: '2' });

isValidGstin('27AAACR5055K1Z7');                                       // true
gstinCheckDigit('27AAACR5055K1Z');                                     // '7'

buildGstin({ pan: 'nope', stateCode: '27' });                          // throws TypeError

Both throw on malformed input rather than returning something plausible-but-wrong, which matters in a factory: a typo in a fixture PAN should blow up the test run loudly, not quietly produce a number that exercises the wrong branch. Full API surfaces on PyPI and npm, with walkthroughs in Python and Node.js and TypeScript.

Wiring it into factories and fixtures

The useful property in a factory is determinism: the same seed produces the same GSTIN, so a failing test reproduces locally and a snapshot does not churn. Build the PAN from a seeded random source rather than a global one.

The fourth character of a PAN encodes entity type, and honouring it makes fixtures read like real data — 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.

# conftest.py
import random
import string

import pytest
from gstin_api import build_gstin

LETTERS = string.ascii_uppercase


def make_pan(rng: random.Random, entity_type: str = "C") -> str:
    """3 letters, entity type, 1 letter, 4 digits, 1 letter -> e.g. AAACR5055K."""
    pick = lambda pool, n: "".join(rng.choice(pool) for _ in range(n))
    return pick(LETTERS, 3) + entity_type + pick(LETTERS, 1) + pick(string.digits, 4) + pick(LETTERS, 1)


@pytest.fixture
def gstin_factory():
    def factory(seed=0, state_code="27", entity_type="C", entity_code="1"):
        rng = random.Random(seed)          # same seed, same GSTIN, every run
        return build_gstin(make_pan(rng, entity_type), state_code, entity_code)

    return factory


def test_vendor_accepts_a_generated_gstin(client, gstin_factory):
    response = client.post("/vendors", json={"gstin": gstin_factory(seed=1)})

    assert response.status_code == 201

The same shape in TypeScript, for a Vitest or Jest suite:

// tests/factories/gstin.ts
import { buildGstin } from '@dlminds/gstin-api';

const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

// A tiny deterministic PRNG - no dependency, reproducible across machines.
function seeded(seed: number) {
  let state = seed + 0x6d2b79f5;
  return () => {
    state = Math.imul(state ^ (state >>> 15), state | 1);
    state ^= state + Math.imul(state ^ (state >>> 7), state | 61);
    return ((state ^ (state >>> 14)) >>> 0) / 4294967296;
  };
}

export function makeGstin({ seed = 0, stateCode = '27', entityType = 'C' } = {}) {
  const rand = seeded(seed);
  const pick = (set: string) => set[Math.floor(rand() * set.length)];
  const take = (set: string, n: number) => Array.from({ length: n }, () => pick(set)).join('');

  // 3 letters, entity type, 1 letter, 4 digits, 1 letter.
  const pan = take(LETTERS, 3) + entityType + pick(LETTERS) + take('0123456789', 4) + pick(LETTERS);

  return buildGstin({ pan, stateCode, entityCode: '1' });
}

makeGstin({ seed: 7 });                      // stable across runs
makeGstin({ seed: 7, stateCode: '29' });     // same PAN, different state

The inputs that actually break validators

One valid GSTIN tells you very little. What catches real bugs is a corpus chosen so that each row breaks a different plausible-but-wrong implementation. This is ours, and you are welcome to copy it wholesale.

InputExpectedWhat it catches
27AAACR5055K1Z7ValidThe happy path — a real, live registration
29AAACI1195H1ZIValidCheck digit is a letter. Breaks any pattern ending [0-9]$
99AAACR5055KAZNValidAlphabetic entity code A, and state code 99
00AAACR5055K1ZNValid checksumCorrect check digit, state code 00 is never issued. Decide deliberately what you report here
27aaacr5055k1z7ValidLowercase must normalise, not fail
 27-AAACR 5055 K1Z7 ValidWhitespace and punctuation people paste out of PDFs
27AAACR5055K1Z0InvalidPattern passes, checksum fails — the case a regex-only validator waves through
27AAACR5055K1Y7InvalidPosition 14 is not Z
27AAACR5055K0Z7InvalidEntity code 0, which is never issued
27AAACR50X5K1Z7InvalidLetter inside the PAN's digit block
271234R5055K1Z7InvalidDigits inside the PAN's letter block
27AAACR5055K1ZInvalid14 characters — off-by-one on the length guard
27AAACR5055K1Z79Invalid16 characters — a validator that only slices the first 15
'' and '   'InvalidEmpty and whitespace-only; must not raise
'not a gstin'InvalidFree text; must not raise either
Every row here has broken a real implementation. The 00 row is the one that separates 'structurally valid' from 'plausible'.

00AAACR5055K1ZN deserves the extra thought. Its check digit is genuinely correct, so "structurally valid" and "could plausibly have been issued" are two different facts about it. Collapse them into one boolean and your validator will eventually disagree with the API behind it. The distinction is the subject of validation versus verification.

Testing the API call without calling it

Generation solves your validation tests. Your verification tests need a different tool, because the correct number of live API calls in a unit suite is zero — live calls cost credits, need a key in CI, and make the suite flaky in a way that has nothing to do with your code. So stub it.

The endpoint is GET https://gstinapi.com/api/get-taxpayer-info/{gstin} with an X-API-Key header. A found record carries taxpayer_data.status_code == 1 alongside name, tradename, status, type, constitution, registrationDate and pradr, the principal address. Full reference in the API documentation.

# test_verification.py
from unittest.mock import patch

from app.gst import verify_gstin

FOUND = {"taxpayer_data": {
    "status_code": 1, "name": "ACME PRIVATE LIMITED", "tradename": "Acme",
    "status": "Active", "constitution": "Private Limited Company",
    "registrationDate": "01/07/2017",
}}

NOT_FOUND = {"taxpayer_data": {"status_code": 0}}


@patch("app.gst.session.get")
def test_maps_an_active_registration(mock_get):
    mock_get.return_value.status_code, mock_get.return_value.json.return_value = 200, FOUND

    result = verify_gstin("27AAACR5055K1Z7")

    assert result.legal_name == "ACME PRIVATE LIMITED"
    assert result.is_active is True


@patch("app.gst.session.get")
def test_generated_gstin_is_not_found_rather_than_an_error(mock_get, gstin_factory):
    mock_get.return_value.status_code, mock_get.return_value.json.return_value = 200, NOT_FOUND

    assert verify_gstin(gstin_factory(seed=3)).outcome == "not_found"


@patch("app.gst.session.get")
def test_never_calls_out_for_a_checksum_invalid_gstin(mock_get):
    assert verify_gstin("27AAACR5055K1Z0").outcome == "invalid_format"
    mock_get.assert_not_called()

The same in Vitest, stubbing fetch. Better still, design the client to take a transport — a transport= callable in Python, a fetch option in Node — so tests inject a fake rather than monkey-patching a global:

 import { expect, it, vi } from 'vitest';
import { verifyGstin } from '../src/gstinClient';
import { makeGstin } from './factories/gstin';

const json = (body: unknown) => ({ ok: true, status: 200, json: async () => body });

it('treats a generated GSTIN as not_found, not as a failure', async () => {
  const fetch = vi.fn().mockResolvedValue(json({ taxpayer_data: { status_code: 0 } }));

  expect((await verifyGstin(makeGstin({ seed: 11 }), { fetch })).outcome).toBe('not_found');
});

it.each([401, 402, 429, 503])('surfaces HTTP %i as an upstream error', async (status) => {
  const fetch = vi.fn().mockResolvedValue({ ok: false, status, json: async () => ({}) });

  expect((await verifyGstin('27AAACR5055K1Z7', { fetch, retries: 0 })).outcome)
    .toBe('upstream_error');
});

Keep exactly one test that touches the network, tag it, and run it on a schedule rather than on every commit. That contract test tells you when the response shape changes; the stubbed suite tells you whether your code is right. They are different jobs. The wider integration pattern is in integrating the API in Node.js.

The line you must not cross

Everything above produces strings that pass validation. It is worth being precise about what that does and does not mean, once, without hedging.

The practical safeguards are dull and effective: prefix generated PANs so they are greppable, never seed them into an environment that sends email or files returns, and fail your build if a fixture GSTIN turns up in production data. If you have received an invoice carrying a number you suspect was fabricated, check it with the GST number search and read how to check if a GST number is fake or how to report a fake GST invoice.

So: no official list, and no need for one. A generator gives you unlimited valid numbers, the corpus above gives you the invalid ones that matter, and stubs keep the network out of your unit tests. For a single number right now, the free GSTIN validator runs entirely in your browser, and the format itself is laid out in GSTIN format explained.

Frequently asked questions

No. GSTN publishes no public developer sandbox and no reserved block of test GSTINs. Every fifteen-character number that satisfies the format either belongs to a real registration or belongs to nobody. That is why the practical answer is to generate checksum-correct numbers yourself and keep them inside your test environment.
You can, and it will work until that company deregisters, migrates, or gets suspended. A test that asserts status equals Active against a live third party is a test that fails on somebody else's schedule, in CI, on a day you were shipping something unrelated. Generate instead, and stub the network.
Take a PAN and a two-digit state code, append the entity code and the literal Z, then compute the modulus-36 check digit over the first fourteen characters. Our libraries do it in one call: build_gstin("AAACR5055K", "27") in Python and buildGstin({ pan, stateCode }) in Node return 27AAACR5055K1Z7.
Almost certainly not. It will return HTTP 200 with taxpayer_data.status_code of 0, meaning no registration matches. That is a legitimate answer rather than an error, and it is not billed — but you should not be calling the live API from unit tests at all.
Generating a string that passes a checksum is arithmetic, and doing it inside a test suite is unremarkable. Presenting that string as your registration on an invoice, a return, or any document shown to a counterparty or the department is falsification. Section 122 of the CGST Act covers issuing an invoice with an incorrect or false GSTIN and the penalties are substantial.
Valid numbers with a letter check digit, an alphabetic entity code, lowercase and punctuated input that must normalise, a structurally valid number with an unissued state code, and the full set of malformed cases: bad check digit, entity code 0, position 14 not Z, letters in the PAN digit block, wrong lengths, empty strings and free text. The table in this article is that corpus.

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