A GSTIN verification API in India takes a 15-character GST Identification Number and tells you, in one HTTP call, whether it exists, who it belongs to and whether the registration is currently active. With StampMitra you send a POST to /v1/verify/gstin with a gstin and an optional businessName, and you get back valid, legalName, tradeName, registrationStatus, principalPlaceOfBusiness and a referenceId for your audit trail.
Most teams use it in two places. At supplier or customer onboarding, it stops a mistyped or fake number from entering your master data. Before you pay a supplier or claim credit on an invoice, it tells you whether the GSTIN printed on that invoice is still in good standing. If all you hold is a PAN, a second endpoint, /v1/verify/pan-to-gstin, lists the GSTINs registered against that PAN.
This guide walks through the structure of a GSTIN, the exact request and response shapes, how to handle active and cancelled results, supplier onboarding and invoice-risk workflows, PAN-to-GSTIN lookups, errors, sandbox testing, the move to production, and a troubleshooting list. Code samples are in cURL, Node.js and Python and follow the GSTIN API reference exactly.
Key takeaways
- A GSTIN has 15 characters: a 2-digit state code, the holder's 10-character PAN, an entity number, a 14th character (commonly
Z) and a final check character. POST /v1/verify/gstinwith theverification.gstin.readscope returnsvalid,legalName,tradeName,registrationStatus,principalPlaceOfBusiness,messageandreferenceId.- Treat
registrationStatusas a gate: proceed only onActive, and route everything else (including anything you have not seen before) to review. - A "not valid" answer is a completed, billable verification with its own
referenceId, not an error. Real errors come back as{ error: { code, message, requestId } }. POST /v1/verify/pan-to-gstinwithverification.pan_to_gstin.readreturns agstinsarray ofgstin,stateandstatus, which helps when a supplier gives you only a PAN.- Sandbox keys (
sk_test_) are free and never debit your wallet. Production keys (sk_live_) need an approved organization and a funded wallet. - A point-in-time check is not a permanent guarantee. Re-verify on a schedule and store the
referenceIdwith the date.
What is a GSTIN and how is the 15-character number structured?
The GSTIN is the registration number issued to a taxpayer under the Goods and Services Tax regime. It is printed on tax invoices, shown on the registration certificate and quoted in GST returns. Because it embeds a PAN, you can derive part of it from other identity data, and that is exactly why it can be sanity-checked before any network call.
The five parts
The structure below is the one described consistently across the public GST explainers I cross-checked. I could not retrieve the body text of the official GST portal pages during research (they render with JavaScript), so please confirm the details against the GST portal before you encode them as hard rules.
| Position | Length | What it holds | Example (from the API docs) |
|---|---|---|---|
| 1 to 2 | 2 | State or union territory code | 07 |
| 3 to 12 | 10 | PAN of the registered person | AAAAA1111A |
| 13 | 1 | Entity number: counts registrations of the same PAN within the state | 1 |
| 14 | 1 | Commonly Z for regular taxpayers; other letters are used for some other registration types | Z |
| 15 | 1 | Check character computed from the first 14 | 1 |
The sample GSTIN in the StampMitra docs is 07AAAAA1111A1Z1. Read left to right it is state code 07, PAN AAAAA1111A, entity number 1, the character Z, and check character 1. In the same documentation the PAN-to-GSTIN sample labels the state of that GSTIN as Delhi, which is consistent with 07 being the Delhi code. The full state code list is published on the GST portal and changes when states or union territories are reorganised, so fetch it from there rather than hard-coding a copy from a blog.
What the structure does and does not tell you
Because characters 3 to 12 are a PAN, two GSTINs that share those ten characters belong to the same PAN holder, whether in the same state or another. The entity number lets one PAN hold several registrations in one state, for example for separate business verticals. This is why a company can legitimately show up with many GSTINs, and why a PAN-to-GSTIN lookup returns a list.
What the structure cannot tell you is whether the number was ever issued. A string can have a perfect shape, a real-looking PAN and a plausible state code and still not exist. The check character catches most single-character typos, but I did not find a published algorithm in a source I could open, so do not write your own checksum and treat a pass as proof. Use shape checks only to reject obvious garbage cheaply, and let the API answer the real question of existence and status.
Why the PAN inside the GSTIN matters
If your onboarding form collects both a PAN and a GSTIN, characters 3 to 12 of the GSTIN should equal the PAN. A mismatch means one of the two was mistyped or belongs to a different entity. You can run this comparison locally with no API cost, and it is covered again in the pre-check section. For the PAN side of the story, see the PAN verification guide.
What does the GSTIN verification API return?
StampMitra exposes GSTIN verification as a single real-time endpoint. The facts below come from the product definition behind the GSTIN verification product page and the reference docs.
Request
| Item | Value |
|---|---|
| Base URL | https://api-b2b.stampmitra.in |
| Method and path | POST /v1/verify/gstin |
| Required scope | verification.gstin.read |
| Auth header | Authorization: Bearer <api-key> (the x-api-key header is also accepted) |
Body field gstin | Required. The 15-character GSTIN |
Body field businessName | Optional. Business name to match |
The request example in the docs is { "gstin": "07AAAAA1111A1Z1", "businessName": "Jane Doe Enterprises" }. Input format is validated up front, so a malformed number fails fast with a 400 instead of being submitted for verification.
Response
{
"referenceId": "SMV-20260927-9K21P4",
"GSTIN": "07AAAAA1111A1Z1",
"valid": true,
"legalName": "Jane Doe Enterprises Private Limited",
"tradeName": "Jane Doe Enterprises",
"registrationStatus": "Active",
"principalPlaceOfBusiness": "123, MG Road, Bengaluru, Karnataka, 560001",
"message": "GSTIN Exists"
}Note that the echoed number is returned under an uppercase key, GSTIN, while the request field is lowercase gstin. If you deserialize into a typed model, map them explicitly. Also note that the sample address is in Bengaluru while the sample GSTIN carries the 07 state code. The documented values are illustrative, but the mismatch is a useful reminder that a registration's state code and the address you collected elsewhere are two separate facts you may want to compare in your own risk rules.
Field by field
referenceId: StampMitra's own transaction id in the formatSMV-YYYYMMDD-XXXXXXX. It is always present, including on a negative result. It is not an idempotency key, so do not use it to deduplicate retries.valid: whether the GSTIN verified successfully.legalNameandtradeName: the names on record. The legal name usually reads like a registered entity and the trade name like the brand.registrationStatus: the registration state. The documented example isActive.principalPlaceOfBusiness: the registered principal place of business as a single string.message: a human-readable summary,GSTIN Existsin the example.
What the docs do not specify
The documentation shows one positive example. It does not publish the full list of possible registrationStatus strings, the body returned when valid is false, or a separate name-match score for businessName. Do not code against values that are not documented. Handle unknown statuses by failing closed, and compare businessName against legalName and tradeName yourself if you need a match decision. The StampMitra docs state that both positive and negative determinations are completed, billable verifications, so a valid: false outcome still arrives with HTTP success and a referenceId.
How do you call the GSTIN API step by step?
Here is the whole path from nothing to a working call. It takes a few minutes in sandbox.
Step 1: Create a sandbox account and key
Sign up at StampMitra Developers with email, mobile and password. You receive an sk_test_ key immediately, with no card and no manual approval, as described in the quickstart. The raw key is shown once, so store it in an environment variable such as STAMPMITRA_API_KEY. The authentication docs cover headers, scopes and IP allowlists.
Step 2: Confirm the key carries the right scope
Keys carry OAuth-style scopes. The GSTIN endpoint requires verification.gstin.read. A valid key without that scope gets a 403 with code FORBIDDEN. If you plan to use PAN-to-GSTIN too, the key also needs verification.pan_to_gstin.read.
Step 3: Send the request
curl -X POST "https://api-b2b.stampmitra.in/v1/verify/gstin" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"gstin":"07AAAAA1111A1Z1","businessName":"Jane Doe Enterprises"}'In Node.js (18 or later, using the built-in fetch):
const res = await fetch("https://api-b2b.stampmitra.in/v1/verify/gstin", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STAMPMITRA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
gstin: "07AAAAA1111A1Z1",
businessName: "Jane Doe Enterprises",
}),
});
const body = await res.json();
if (!res.ok) {
// Every error is { error: { code, message, requestId } }
throw new Error(`${body.error.code}: ${body.error.message} (${body.error.requestId})`);
}
console.log(body.registrationStatus, body.referenceId);In Python:
import os
import requests
resp = requests.post(
"https://api-b2b.stampmitra.in/v1/verify/gstin",
headers={"Authorization": f"Bearer {os.environ['STAMPMITRA_API_KEY']}"},
json={"gstin": "07AAAAA1111A1Z1", "businessName": "Jane Doe Enterprises"},
timeout=30,
)
body = resp.json()
if not resp.ok:
err = body["error"]
raise RuntimeError(f"{err['code']}: {err['message']} ({err['requestId']})")
print(body["registrationStatus"], body["referenceId"])Step 4: Read the result and decide
Check valid first, then registrationStatus, then compare names if your policy needs it. The next section turns that into a decision table.
Step 5: Persist the evidence
Save the referenceId, the date and time of the call, the GSTIN, and the fields you relied on. If an auditor or your own finance team later asks why a vendor was approved, this record answers the question. Keep it minimal, as discussed in the compliance section. Call the API from your server only. The key must never ship in a mobile app or browser bundle.
How should you treat Active, Cancelled and other statuses?
The status field is where verification becomes a business decision. A number can exist and still be a bad counterparty.
What the public sources say about status
Public GST explainers describe the portal's taxpayer search as showing a registration status such as Active, Suspended or Cancelled, with a cancellation date where relevant. The cancellation of a registration is dealt with in Section 29 of the CGST Act, 2017, which covers cancellation by the officer or at the taxpayer's request. I could not open the statute text during research, so confirm the exact provisions with the CBIC site or your tax advisor. What the StampMitra API documents is only the sample value Active. Do not assume the API returns the exact same strings the portal displays. Log the raw value and build your allow-list from what you actually observe.
A conservative decision table
| Result | Suggested action | Why |
|---|---|---|
valid: true, registrationStatus: "Active" | Proceed, store referenceId | The number exists and is in good standing at the time of the call |
valid: true, any other status | Hold for manual review | Not documented as safe. A cancelled or suspended taxpayer is a different counterparty from an active one |
valid: false | Reject or ask the user to re-enter | Number not verified. Billable, completed outcome |
| HTTP 4xx or 5xx | Do not treat as a business answer | The call itself failed. Retry or fix the request |
Fail closed on unknown values
The most dangerous pattern is if (status !== "Cancelled") approve(). It approves every status you did not think of. Invert it:
function gstDecision(r) {
if (!r.valid) return "REJECT";
if (r.registrationStatus === "Active") return "APPROVE";
return "REVIEW"; // cancelled, suspended or anything new goes to a human
}Status is a snapshot
An Active result describes the moment of the call. A supplier can be active when you onboard it and cancelled six months later. That is why the invoice-risk section recommends a re-check close to payment, not only at onboarding. Cancellation dates and the effect on credit for past invoices are tax questions that depend on facts and on the law as it stands. Ask a qualified chartered accountant rather than deriving a rule from an API field.
Name comparison
Legal names on record are often long and formal, for example "Jane Doe Enterprises Private Limited", while an invoice or onboarding form carries the trade name. Compare after normalising case, punctuation and common suffixes such as "Pvt Ltd" and "Private Limited". Accept a match against either legalName or tradeName. Treat a mismatch as a review reason, not an automatic rejection, because a typo in a form is far more common than fraud.
How do you use GSTIN verification in supplier onboarding?
Supplier onboarding is the highest-value use case because the cost of a bad record compounds. A wrong GSTIN in your vendor master gets copied into purchase orders, payment advice and tax reconciliation.
A practical onboarding flow
- The supplier submits legal name, PAN, GSTIN, bank details and an address through your portal.
- Your backend runs a local shape check and the PAN-inside-GSTIN comparison. Failures return an inline form error, with no API call.
- Your backend calls
POST /v1/verify/gstin. - The decision function above sets the vendor state to
approved,revieworrejected. - You store
referenceId, the timestamp and the relevant fields on the vendor record. - For vendors in
review, a finance user sees the returned legal name, trade name, status and place of business next to what the supplier typed.
If you also verify the PAN and bank account, you can run those checks alongside, using the PAN verification API and the bank account verification API. The business verification guide covering GSTIN, CIN and Udyam shows how to combine registries when the counterparty is a company or a micro enterprise.
A worked example
Suppose "Nimbus Packaging Co", an invented vendor, submits GSTIN 07AAAAA1111A1Z1 and PAN AAAAA1111A. The local check confirms that characters 3 to 12 equal the PAN. The API call returns:
{
"referenceId": "SMV-20260927-9K21P4",
"GSTIN": "07AAAAA1111A1Z1",
"valid": true,
"legalName": "Jane Doe Enterprises Private Limited",
"tradeName": "Jane Doe Enterprises",
"registrationStatus": "Active",
"principalPlaceOfBusiness": "123, MG Road, Bengaluru, Karnataka, 560001",
"message": "GSTIN Exists"
}Neither name resembles "Nimbus Packaging Co". Even though valid is true and the status is Active, your name rule routes the vendor to review. A finance user may discover that the vendor is operating under a different trade name, or that the GSTIN was copied from another party. The point is that "exists and active" and "belongs to the party in front of me" are different questions, and the name comparison is what answers the second.
Capturing consent and purpose
You are checking a business registration, but the legal and trade names of a sole proprietor are personal data in practice. State in your onboarding terms why you verify, keep the lookup tied to that purpose, and do not reuse results for unrelated profiling. See the compliance section for more.
Where this fits in a broader KYC stack
Fintech and NBFC teams usually pair business checks with individual KYC. The KYC API use case for fintech and NBFC describes that wider pattern.
How does GSTIN verification reduce invoice and ITC risk?
Input tax credit is the mechanism that lets a registered buyer offset tax paid on purchases against its own output liability. Whether credit is available on a particular invoice depends on conditions in the CGST Act, 2017 (Section 16 is the eligibility provision), including whether the supplier actually paid the tax. I could not open the statute text, so the conditions are not restated here. Discuss them with a chartered accountant. What an API can do is make the supplier-side facts visible to you at the right moments.
What the API can and cannot prove
| The API can show | The API cannot show |
|---|---|
| Whether the GSTIN exists | Whether the supplier filed its returns |
| The registered legal and trade names | Whether the supplier paid the tax on a given invoice |
| The status at the time of the call | Whether your credit claim will be accepted |
| The registered principal place of business | The supplier's financial health or intent |
An active registration is a necessary signal for a healthy supplier, not a sufficient one. Do not present a verification result to your tax team as an assurance of credit eligibility.
Three checkpoints that catch most problems
- At onboarding. Reject bad numbers before they reach the vendor master.
- At invoice receipt. When an invoice arrives, compare the GSTIN on it with the one on file. If they differ, verify the new one before booking.
- Before payment. Re-verify vendors above a value threshold, or any vendor not checked within your chosen window, right before payment release.
A scheduled re-verification job
A nightly or weekly job over your active vendors turns status drift into a work queue. Respect the documented rate limit: the 429 example in the docs reads "60 requests per 60 seconds for this endpoint", so pace yourself rather than bursting.
import os, time, requests
URL = "https://api-b2b.stampmitra.in/v1/verify/gstin"
HEADERS = {"Authorization": f"Bearer {os.environ['STAMPMITRA_API_KEY']}"}
def check(gstin):
r = requests.post(URL, headers=HEADERS, json={"gstin": gstin}, timeout=30)
if r.status_code == 429:
time.sleep(10)
r = requests.post(URL, headers=HEADERS, json={"gstin": gstin}, timeout=30)
r.raise_for_status()
return r.json()
flagged = []
for vendor in load_active_vendors(): # your own data access
result = check(vendor["gstin"])
if not result["valid"] or result["registrationStatus"] != "Active":
flagged.append((vendor["id"], result["referenceId"]))
time.sleep(1.1) # stay under 60 calls per minuteEach successful call is metered at the rate shown by ₹5 in production, so choose your re-check frequency by risk tier. Check high-value vendors more often and dormant ones rarely.
Caching and how often to re-check
Every call is metered, so a sensible cache saves money without hiding change. A simple approach is to store the last result per GSTIN with its timestamp and a time-to-live that depends on the vendor's risk tier. A high-value or new vendor might be re-checked before every payment run, a steady mid-tier vendor monthly, and a dormant vendor only when it next appears on an invoice. Never serve a cached Active to a payment decision that is far older than your policy allows, and show the age of the check next to the status in your finance screens so a reviewer can judge it at a glance.
A worked invoice-receipt example
Imagine your accounts payable inbox receives an invoice from an invented vendor, "Kaveri Tools Pvt Ltd", quoting a GSTIN that differs from the one in your vendor master. Your automation first normalises the number and checks its shape and PAN block. Because the new number carries a different PAN block from the one on file, it flags a possible wrong-party invoice before spending an API call. A reviewer then confirms with the vendor. If the vendor says it has a second registration in another state, you run /v1/verify/gstin on the new number, store the referenceId, and attach the second GSTIN to the same vendor record. If the vendor cannot explain the difference, the invoice stays unbooked. The check did not decide anything alone; it gave the reviewer facts quickly.
Acting on a flag
A flagged vendor should trigger a human workflow, not an automatic payment block with no explanation. Show the finance owner the old and new status, the referenceId and the date, and let them contact the supplier. Whether to hold a payment or reverse a credit is a commercial and tax judgement.
What does PAN-to-GSTIN lookup do and when do you need it?
Sometimes you have a PAN and need to know which GST registrations sit behind it. Because a GSTIN embeds a PAN, one PAN can map to many GSTINs across states and, within a state, across entity numbers.
The endpoint
| Item | Value |
|---|---|
| Method and path | POST /v1/verify/pan-to-gstin |
| Required scope | verification.pan_to_gstin.read |
| Body field | pan (required), format ABCDE1234F |
| Metered event | pan.to_gstin |
curl -X POST "https://api-b2b.stampmitra.in/v1/verify/pan-to-gstin" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pan":"ABCDE1234F"}'The documented response:
{
"referenceId": "SMV-20260927-1L67Z5",
"found": true,
"gstins": [
{ "gstin": "07AAAAA1111A1Z1", "state": "Delhi", "status": "Active" }
]
}Here found signals whether any registration was located, and each element of gstins carries the number, the state name and a status. The sample PAN and the GSTIN in the sample do not literally match characters 3 to 12, which is a sign that these are placeholder values. Do not build a test that expects the sample PAN to be embedded in the sample GSTIN. In real data it will be.
When it is the right tool
- A customer gives only a PAN at onboarding and you must collect the GSTIN for invoicing. Show the returned list and ask them to pick.
- You want to see how many registrations a supplier operates before you accept a single GSTIN as representative.
- You are building a reconciliation tool that needs every registration for a group entity.
When it is not
If you already hold a GSTIN, call /v1/verify/gstin directly. It returns names and place of business, which the lookup does not. If you hold a Udyam registration rather than a GST number, the relevant tools are PAN to Udyam and the Udyam verification API. The PAN to GSTIN reference has the full parameter table.
Combining the two calls
A common sequence is PAN to GSTIN to GSTIN verify. First list the registrations. For the one the user selected, call the verification endpoint to fetch legal name, trade name and place of business. This costs two calls, so use it when the user really only knows the PAN. Verify the PAN itself first if you also need to confirm the holder's name, using the PAN verification flow.
async function post(path, payload) {
const res = await fetch(`https://api-b2b.stampmitra.in${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STAMPMITRA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body;
}
const lookup = await post("/v1/verify/pan-to-gstin", { pan: "ABCDE1234F" });
if (lookup.found) {
const active = lookup.gstins.filter((g) => g.status === "Active");
// Let the user choose from `active`, then:
const detail = await post("/v1/verify/gstin", { gstin: active[0].gstin });
console.log(detail.legalName, detail.registrationStatus);
}What errors can the API return and how do you handle them?
Every error from this API is normalised to one shape, produced by a global exception filter on the server: { "error": { "code": "...", "message": "...", "requestId": "..." } }. It does not leak stack traces or raw database errors. Log requestId with every failure, because it is the handle StampMitra support needs to trace a call.
Errors documented for the GSTIN endpoint
| HTTP | Code | Documented message or label | What to do |
|---|---|---|---|
| 400 | BAD_REQUEST | gstin must match the format 07AAAAA1111A1Z1 | Fix the input. Do not retry unchanged |
| 401 | UNAUTHORIZED | Missing or invalid API key. | Check the key, the header name and the environment |
| 402 | PAYMENT_REQUIRED | Insufficient fulfilment wallet balance to cover this call. | Top up the wallet, then retry |
| 403 | FORBIDDEN | API key is valid but lacks the required scope for this endpoint. | Add the verification.gstin.read scope to the key |
| 429 | TOO_MANY_REQUESTS | Rate limit exceeded: 60 requests per 60 seconds for this endpoint. | Back off and retry with spacing |
| 502 | INTERNAL_ERROR | Unable to reach the order/customer service right now | Upstream unreachable. Retry with backoff |
Retry policy
Retry only what can change. A 400, 401 and 403 will fail identically until you fix something, so retrying them wastes calls. A 429 and a 502 are transient. Use exponential backoff with jitter and a small cap on attempts.
async function withRetry(fn, max = 4) {
for (let i = 0; i < max; i++) {
try {
return await fn();
} catch (e) {
const transient = /TOO_MANY_REQUESTS|INTERNAL_ERROR/.test(e.message);
if (!transient || i === max - 1) throw e;
await new Promise((r) => setTimeout(r, 500 * 2 ** i + Math.random() * 250));
}
}
}The difference between a failed call and a negative answer
A 4xx or 5xx means the call did not complete. A valid: false in a 200 response means the call completed and the answer was negative. Only the second is a verification outcome, and per the product notes it is billable. Your code should keep these two paths separate in logs and dashboards. The guide to API errors and handling goes deeper on logging and alerting patterns shared by every StampMitra endpoint.
Metering and the 402
Usage is recorded, and the wallet debited for priced services, only after a successful verification completes. A 402 therefore signals that the wallet could not cover the call, so plan for it as an operational alert rather than a data problem. Monitor your wallet balance so a 402 does not surprise your onboarding flow on a busy day.
How do you validate input before calling the API?
Pre-checks cost nothing and remove most 400s and wasted calls. They also give users instant feedback instead of a round trip.
Normalise first
Trim whitespace and convert to uppercase. Users paste numbers with trailing spaces or in lowercase, and the API expects the canonical form shown in its docs.
A shape check you can run locally
The API fails fast on a malformed number, so your pre-check only needs to reject the obvious. This pattern encodes the structure from earlier: two digits, a PAN-shaped block of five letters, four digits and a letter, then entity number, a 14th character and a check character.
const GSTIN_SHAPE = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z][0-9A-Z][0-9A-Z]$/;
function preCheck(gstinRaw, panRaw) {
const gstin = gstinRaw.trim().toUpperCase();
if (!GSTIN_SHAPE.test(gstin)) return { ok: false, reason: "FORMAT" };
if (panRaw && gstin.slice(2, 12) !== panRaw.trim().toUpperCase()) {
return { ok: false, reason: "PAN_MISMATCH" };
}
return { ok: true, gstin };
}I kept positions 13 to 15 deliberately loose ([0-9A-Z]). Public sources disagree on whether the 14th character is always Z, and I did not find an official statement I could open, so a stricter rule could reject genuine numbers. If the API's own format validator is stricter than your pattern, you simply get a 400, which is safe.
What not to pre-check
Do not compare the state code against a hard-coded list of your own, and do not implement a checksum. Both are easy to get subtly wrong and add nothing the API does not already do. If you must check the state code, read the list from the GST portal and date-stamp your copy.
Pre-check outcomes
| Local result | Action |
|---|---|
| Shape fails | Inline form error, no API call |
| PAN does not match characters 3 to 12 | Ask the user to correct either field, no API call |
| Passes | Call the API |
How do you test GSTIN verification in sandbox?
Sandbox lets you build and test the full integration without spending anything. The pricing page states that sandbox calls run against sandbox data and never debit your wallet.
What to test
- The happy path with the documented sample
07AAAAA1111A1Z1. - A malformed number such as
07AAAAA1111A1Z(14 characters), to see the 400 body and confirm your form shows a friendly message. - A missing or wrong key to see the 401, and a key without the scope to see the 403.
- Your handling of
valid: false,registrationStatusvalues other thanActive, and a 429 triggered by a loop. - A simulated 502 in your own tests by stubbing the HTTP client, since you cannot rely on causing one on demand.
Be careful about sandbox data
Sandbox responses come from sandbox data, not live government records. A number that returns an active result there says nothing about a real supplier. Never carry sandbox results into production decisions. Never type a real taxpayer's GSTIN into a test unless you have a purpose to do so, and use the sample values from the docs as the brief for this site requires.
A minimal automated test
def test_gstin_active(client):
body = client.verify_gstin("07AAAAA1111A1Z1")
assert body["valid"] is True
assert body["registrationStatus"] == "Active"
assert body["referenceId"].startswith("SMV-")
def test_gstin_format_error(client):
with pytest.raises(ApiError) as e:
client.verify_gstin("NOT-A-GSTIN")
assert e.value.code == "BAD_REQUEST"Tools that speed this up
The Postman collection lets you try the endpoint without writing code, the SDKs page lists client libraries, and the MCP server lets an AI assistant call sandbox endpoints while you build. The sandbox versus production keys guide compares the two environments in detail.
A note on quota wording: the product pages describe sandbox usage in slightly different ways, so check your dashboard for the current allowance instead of assuming a number.
How do you move GSTIN verification to production?
Moving to production changes the key, the data source, the billing and the stakes. Treat it as a launch, not a config flip.
Checklist
- Apply for production access from the dashboard. Production keys require an organization that has passed StampMitra's onboarding verification, as described in the authentication docs. Read the developer onboarding verification policy so you know what to prepare.
- Create a production key (
sk_live_) with only the scopes you need:verification.gstin.read, plusverification.pan_to_gstin.readif you use the lookup. Sensitive scopes can be restricted to an IP allowlist, so use it for your server addresses. - Fund the wallet. Each successful production call is deducted from your API credit wallet at the per-call rate on the pricing page. Today the GSTIN check is priced at
₹5per successful call and PAN to GSTIN at₹5. - Move the key into your secret manager, not your repo, and rotate it on a schedule. The API security policy describes the expectations.
- Add monitoring: error rate by code, 402 alerts for a low wallet, 429 counts, and latency.
- Run a smoke test with a single known, real, business-owned GSTIN you are entitled to check, and confirm names and status against what you see in your own records.
Environment hygiene
Keep sandbox and production keys in separate variables and make your code print which environment it is using at startup. The prefix tells you: sk_test_ is sandbox, sk_live_ is production. A fast guard in CI is to refuse to deploy if a production build contains a test-prefixed key, and vice versa.
Data handling in production
Production results contain real business details. Restrict who can read stored verification records, set a retention period tied to your purpose, and avoid logging full response bodies in general-purpose logs. The acceptable use policy lists permitted and prohibited uses, so read it before you build a bulk or reseller-style flow.
Which approach should you choose? A decision guide
Pick the narrowest call that answers your question. Calls are metered, so unnecessary lookups cost money without adding certainty.
When to use which endpoint
| You have | You need | Use |
|---|---|---|
| A GSTIN | Existence, names, status, place of business | POST /v1/verify/gstin |
| Only a PAN | The GSTINs registered against it | POST /v1/verify/pan-to-gstin |
| A PAN and a GSTIN | Confirm they belong together | Local compare of characters 3 to 12, then verify the GSTIN |
| A PAN and a person's name | Identity of an individual | PAN verification |
| A CIN | Company registry data | CIN verification |
| A Udyam number | Micro, small or medium enterprise status | Udyam verification |
When GSTIN verification is a good fit
- You onboard suppliers, distributors or B2B customers and need clean tax identifiers.
- You pay vendors regularly and want a status check close to payment.
- You issue B2B invoices and want to confirm the buyer's GSTIN before it goes on the document.
When it is not enough, or not appropriate
- You need proof of the supplier's return filing or tax payment. This API does not provide that.
- You need to verify an individual who is not GST registered. A GSTIN exists only for registered persons.
- You want a one-time check to stand in for ongoing monitoring. Status changes, so schedule re-checks.
- You need a legal or tax opinion. Ask a qualified chartered accountant or advocate.
Build a rule set, not a boolean
Combine signals into a small, explicit policy: shape check passes, PAN matches, valid is true, status is Active, and a name match against either name. Each failing signal gets a distinct reason code so reviewers know what to look at. The PAN side of that policy is covered in the PAN verification guide, and the wider set of registries in the business verification guide.
What are the common mistakes and how do you troubleshoot them?
Most integration problems fall into a short list. The table pairs the symptom with the most likely cause and the fix.
Symptom and cause
| Symptom | Likely cause | Fix |
|---|---|---|
400 BAD_REQUEST on a number that looks right | Lowercase letters, trailing space, wrong length or a transposed character | Trim, uppercase and re-check the 15 characters |
401 UNAUTHORIZED | Missing header, a sk_test_ key pointed at a production workflow, or a revoked key | Confirm Authorization: Bearer ... or x-api-key, and the key's environment |
403 FORBIDDEN | Key lacks verification.gstin.read (or verification.pan_to_gstin.read) | Recreate or edit the key with the scope |
402 PAYMENT_REQUIRED | Production wallet is low | Top up from the dashboard billing area |
429 TOO_MANY_REQUESTS | A loop or batch exceeded 60 requests per 60 seconds on the endpoint | Pace requests, queue, add backoff |
502 INTERNAL_ERROR | Upstream verification service unreachable | Retry with backoff, then contact support with the requestId |
valid: false but you believe the number is real | Typo, a number that was never issued, or a number from the wrong context | Re-confirm from the original registration certificate or invoice |
| Name mismatch on a real vendor | Trade name versus legal name, abbreviation, punctuation | Compare against both names after normalising |
GSTIN field is undefined in your code | You read lowercase gstin from the response | The response key is uppercase GSTIN |
Unexpected registrationStatus string | A value you did not expect | Log it, route to review, add it to your policy once understood |
Design for partial failure in batches
Bulk jobs fail in the middle. Persist progress per GSTIN so a crash does not make you repeat paid calls, and record the outcome class for each item: verified, not valid, or call failed. Re-queue only the call-failed items. Because a valid: false result is a completed verification, re-running it immediately will cost again and return the same answer, so route those items to a human instead of looping. Keep the batch size small enough that your pacing against the 60-per-minute example limit finishes inside your maintenance window, and make the job safe to stop and resume.
Questions worth asking your own team before launch
Who owns a flagged vendor, and how fast must they respond? What is the maximum age of a check that finance will accept before releasing a payment? Who may override a rejection, and is that override logged with a reason? Which fields from the response are you allowed to keep, and for how long? Answering these before go-live turns the API from a data source into a working control, and it is usually the policy answers, not the code, that decide whether the integration is trusted.
Mistakes teams make repeatedly
- Treating the check as permanent. Status changes, so keep a re-verification schedule.
- Auto-approving on "not cancelled". Use an allow-list of
Active. - Retrying every failure. Retrying a 400 simply repeats it.
- Using
referenceIdas an idempotency key. It is a transaction id for your records, not a deduplication token. - Calling from the browser. Your key would be exposed. Call from your backend.
- Logging full responses everywhere. Legal names and addresses of sole proprietors are personal data.
- Mixing environments. A test key in production fails with 401, and production data in tests creates cost and risk.
What to send when you contact support
Write to [email protected] with the requestId from the error body, the endpoint, the time of the call and the environment. Never include your API key or a full list of real taxpayer numbers. The support page lists the other channels.
What security and compliance points should you plan for?
StampMitra is a private LegalTech platform operated by Bani Global Industries LLP. It is not a government agency, and its responses are not a government certificate or legal advice. Treat results as a verification signal you combine with your own policy, and take legal or tax questions to qualified counsel.
Personal data and the DPDP Act, 2023
GST registrations of proprietors and small partnerships can reveal names and home addresses. The Digital Personal Data Protection Act, 2023 sets out duties for handling digital personal data. I could not open the statute text during research, so I will not cite specific sections. As general good practice: tell people why you verify, collect only what you need, use results only for that purpose, limit who can read them, and delete them when the purpose ends. Confirm your obligations with qualified counsel.
Practical controls
| Control | What it looks like |
|---|---|
| Key handling | Keep sk_live_ keys in a secret manager, rotate them, never ship them to clients |
| Least privilege | One key per service with only the scopes it needs |
| IP allowlist | Restrict sensitive scopes to your server IPs |
| Storage minimisation | Keep referenceId, status, timestamp and the fields you relied on. Drop the rest |
| Access control | Only finance and compliance roles can read vendor verification records |
| Audit | Log who triggered a check and which referenceId came back |
| Vendor communication | Do not accuse a supplier of fraud on the basis of one mismatch. Ask them to explain |
StampMitra's developer privacy policy and data processing addendum explain how the platform handles data on your behalf. Read them alongside your own notices.
Fair use
Do not use verification to enumerate or scrape taxpayers, to build lists you have no relationship with, or to make automated adverse decisions about individuals without human review. The acceptable use policy sets the rules.
How StampMitra Helps
StampMitra's developer platform gives you the two GST calls described above, behind the same authentication, error shape and metering as every other verification product. You get POST /v1/verify/gstin for number-level checks and POST /v1/verify/pan-to-gstin for PAN-first lookups, each protected by its own scope, each returning a referenceId you can store as evidence. Errors always arrive as { error: { code, message, requestId } }, so one handler serves every endpoint.
You can start without a sales call. A free sandbox key arrives as soon as you sign up, and sandbox calls never debit your wallet. When you are ready, apply for production from the dashboard, fund the wallet and switch to an sk_live_ key. The per-call price is listed on the pricing page and currently reads ₹5 for the GSTIN check and ₹5 for the lookup. The same key can also call PAN, bank account, CIN and Udyam verification, so a supplier file can be checked end to end from one integration. See the full product catalogue for the list.
Honest limits matter here. StampMitra returns the fields documented above. It does not currently document a separate name-match score on the GSTIN response, a list of every possible registrationStatus value, or any return-filing information, so your rules should not depend on them. A result describes the moment of the call and is not a guarantee about future status, tax credit or the supplier's conduct. StampMitra is a private LegalTech platform, not a government body, and nothing in this guide is legal or tax advice.
What is next, as plans rather than promises: we intend to keep widening the documentation with more worked examples and to publish additional sibling guides for common onboarding flows. If a field or behaviour you need is missing, tell us through the support page or at [email protected]. Requests from real integrations help us decide what to document and build next.
Ready to try it? Create a free sandbox account, follow the quickstart, and call the GSTIN endpoint with the sample number in a few minutes.
API reference at a glance
POST /v1/verify/gstinRequest parameters
| Field | Type | Description |
|---|---|---|
Authorizationrequired | header | Bearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted. |
gstinrequired | string | 15-character GSTIN. |
businessName | string | Business name to match. |
POST /v1/verify/gstin{
"referenceId": "SMV-20260927-9K21P4",
"GSTIN": "07AAAAA1111A1Z1",
"valid": true,
"legalName": "Jane Doe Enterprises Private Limited",
"tradeName": "Jane Doe Enterprises",
"registrationStatus": "Active",
"principalPlaceOfBusiness": "123, MG Road, Bengaluru, Karnataka, 560001",
"message": "GSTIN Exists"
}POST /v1/verify/gstinGSTIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
POST /v1/verify/pan-to-gstinRequest parameters
| Field | Type | Description |
|---|---|---|
Authorizationrequired | header | Bearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted. |
panrequired | string | PAN to resolve to associated GSTIN(s). |
POST /v1/verify/pan-to-gstin{
"referenceId": "SMV-20260927-1L67Z5",
"found": true,
"gstins": [
{
"gstin": "07AAAAA1111A1Z1",
"state": "Delhi",
"status": "Active"
}
]
}POST /v1/verify/pan-to-gstinPAN to GSTIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
Frequently asked questions
What is a GSTIN verification API?
It is an HTTP endpoint that accepts a 15-character GSTIN and returns whether the number exists, the registered legal and trade names, the registration status and the principal place of business, so you can check a business programmatically.
How is a GSTIN structured?
A GSTIN has 15 characters: a 2-digit state code, the 10-character PAN of the holder, an entity number, a 14th character commonly Z, and a final check character. Confirm details against the GST portal.
Which StampMitra endpoint verifies a GSTIN?
Send a POST request to /v1/verify/gstin on https://api-b2b.stampmitra.in with a gstin field and an optional businessName. The API key needs the verification.gstin.read scope and is sent as a Bearer token or x-api-key header.
What fields does the GSTIN response contain?
The documented response contains referenceId, GSTIN, valid, legalName, tradeName, registrationStatus, principalPlaceOfBusiness and message. The referenceId is StampMitra's transaction id, always present, including when the result is negative.
How should I handle an active versus cancelled GSTIN?
Fail closed: approve only when valid is true and registrationStatus is Active, and send every other value, including ones you have not seen before, to manual review. Status is a snapshot, so re-verify near payment.
Can I find GSTINs if I only have a PAN?
Yes. POST a pan to /v1/verify/pan-to-gstin with the verification.pan_to_gstin.read scope. It returns found and a gstins array with gstin, state and status for each registration linked to that PAN.
Does GSTIN verification prove a supplier's invoices are eligible for input tax credit?
No. It shows that a GSTIN exists and its status at the time of the call. It does not show return filing or tax payment, so ask a chartered accountant about credit eligibility.
Is a negative GSTIN result billable?
According to the StampMitra product notes, both positive and negative determinations are completed, billable verifications and carry a referenceId. Calls rejected earlier, such as a 400 for a bad format, are errors rather than results.
Can I test GSTIN verification for free?
Yes. Create a sandbox account to get an sk_test_ key. Sandbox calls run against sandbox data and never debit your wallet. Production needs an approved organization, an sk_live_ key and a funded wallet.
What happens if I exceed the rate limit?
The API returns HTTP 429 with code TOO_MANY_REQUESTS. The documented example mentions 60 requests per 60 seconds for the endpoint, so pace batch jobs, add backoff and retry only after a short delay.
Sources
Verified on 2026-10-16 unless noted.
- StampMitra API product definitions for
gstinandpan-to-gstin, scopes, parameters, response examples and error examples: the GSTIN reference and PAN to GSTIN reference, with authentication in /docs/auth and pricing in /pricing. - GST Portal, Goods and Services Tax Network: www.gst.gov.in and its Search Taxpayer service. The pages opened, but their body text is rendered by JavaScript and could not be read in the research tool. The GSTIN structure and status descriptions in this guide therefore rest on several consistent public explainers and are flagged for confirmation against the portal.
- GST Portal user tutorials: tutorial.gst.gov.in, which lists a "GSTIN format" topic. Its body text could not be read, so it was not relied on for specifics.
- Central Board of Indirect Taxes and Customs, GST section: cbic-gst.gov.in. The CGST Act, 2017 (Sections 16 and 29 are mentioned only by topic) could not be opened in full, so no statutory wording is quoted. Confirm with a qualified chartered accountant.
- Digital Personal Data Protection Act, 2023: referred to only at the level of general good practice. The statute text was not opened, so no section is cited.