StampMitraDevelopers

Guides

Verify a Business in India: GSTIN, CIN and Udyam API Guide

To verify a business in India by API, pick the identifier that proves each fact: CIN for company incorporation, GSTIN for GST registration and place of business, Udyam for self-declared MSME status, and PAN as the link between them. Run them as an ordered waterfall per entity type, stop at the first hard failure, and send name mismatches to review.

By StampMitra Developer TeamLast updated: Read this in Hindi

To verify a business in India through an API, you do not call one "business" endpoint. You pick the identifier that proves the fact you care about and call the matching lookup: a GSTIN to prove a tax registration and a registered address, a CIN to prove a company exists on the Registrar of Companies record, a Udyam Registration Number to prove MSME status, and a PAN to tie all of them to one legal taxpayer. The StampMitra verification API exposes each of these as a separate POST /v1/verify/... call, so a KYB flow is a short sequence of calls, ordered by entity type.

The order matters for both accuracy and cost. A company should start at CIN, a proprietor at PAN or GSTIN, and an MSME claim should be checked last because it is the least decisive. Each successful call is billed per product, and a negative result such as valid: false is still a completed, billable call, so a good waterfall stops early when an earlier step fails and uses the cross-lookups (pan-to-gstin, pan-to-udyam) only when the customer cannot give you the identifier directly.

This guide covers what each identifier does and does not prove, a waterfall for companies, LLPs, proprietorships and MSMEs, working request and response bodies in cURL, Node.js and Python, how to compare names and handle mismatches, error handling, sandbox testing, and the checks before you go live. It is general information for engineers, not legal or compliance advice.

Key takeaways

  • A GSTIN proves a GST registration and its status at one state and one place of business. It does not prove the company is incorporated, and it does not prove MSME status.
  • A CIN proves incorporation of a company on the Registrar of Companies record and returns status and directors. LLPs and proprietorships do not have a CIN, so the CIN call is only for companies.
  • A Udyam Registration Number (URN) records a self-declared MSME registration and its size category. Treat it as a supporting claim, not as identity proof.
  • PAN is the common thread: the GSTIN embeds a PAN, and pan-to-gstin and pan-to-udyam let you walk from one PAN to the other registrations.
  • Sequence calls from the most decisive and cheapest-to-fail to the least. Stop the waterfall at the first hard failure.
  • Every verification response carries a referenceId (format SMV-YYYYMMDD-XXXXXXX). Store it with your decision as an audit trail.
  • A valid: false or found: false result is billable. Validate formats yourself before calling, because a malformed value fails with a 400 without a verification being run.
  • Compare names with tolerance and route near-misses to a human. Never auto-reject on a single trade name versus legal name difference.

Which identifier proves what?

Four identifiers show up in nearly every Indian KYB flow, and they answer different questions. Mixing them up is the most common design mistake: teams ask for a GSTIN to "prove the company is real" and end up trusting a registration that says nothing about incorporation.

IdentifierIssued byFormatWhat it provesWhat it does not prove
PANIncome Tax Department10 characters, ABCDE1234FA taxpayer identity and its holder categoryThat the entity trades, is incorporated, or is GST registered
GSTINGST system (GSTN)15 characters, e.g. 07AAAAA1111A1Z1A GST registration for one PAN in one state, its status and principal place of businessIncorporation, ownership, directors, MSME size
CINMinistry of Corporate Affairs (Registrar of Companies)21 characters, e.g. U74999DL2015PTC284009A company registered under company law, with status and directorsGST registration, tax compliance, anything about LLPs or proprietors
Udyam numberMinistry of MSMEUDYAM-MH-12-1234567A self-declared MSME registration and its categoryIdentity, solvency, or that the declared figures are audited

PAN: the taxpayer anchor

The Income Tax Department issues the PAN, and the fourth character of a PAN is commonly described as identifying the holder category (individual, company, firm and so on). I could not open the official Income Tax portal page that lists those codes, so check the current list there before encoding it in your own logic. What matters for KYB is that the same ten characters sit inside every GSTIN, and that the Udyam portal collects PAN as well. That is why PAN works as the join key between registrations, which the PAN verification API guide covers in depth.

GSTIN: tax registration, per state

Published GST guidance describes a GSTIN as a 15-character number: a two-digit state code, the holder's ten-character PAN, an entity number for repeat registrations under the same PAN in a state, a default character, and a check character. A business with operations in three states therefore holds three GSTINs under one PAN. This is why "the GSTIN is valid" and "this PAN is the right company" are different statements. The StampMitra response returns legalName, tradeName, registrationStatus and principalPlaceOfBusiness. See the GSTIN verification guide for the field-by-field walkthrough.

CIN: incorporation

The Ministry of Corporate Affairs assigns a CIN to a company when the Registrar of Companies registers it. Commonly published explanations break the 21 characters into listing status, industry code, state, year of incorporation, company type and a registration number. In the sample U74999DL2015PTC284009, the leading U marks an unlisted company, DL is Delhi, 2015 is the year, and PTC marks a private company. Because I could not open the MCA site directly for this guide, treat the segment meanings as secondary-source detail and confirm against MCA when it matters.

Udyam: MSME status

The Udyam portal run by the Ministry of MSME describes registration as online, paperless and free, based on self-declaration. It collects Aadhaar, and PAN and GSTIN where applicable. From 1 April 2025 the portal publishes revised classification limits, shown below. Size labels can change by notification, so read the live portal rather than hardcoding thresholds.

CategoryInvestment and turnover up to 31 March 2025From 1 April 2025
Micro₹1 crore / ₹5 crore₹2.5 crore / ₹10 crore
Small₹10 crore / ₹50 crore₹25 crore / ₹100 crore
Medium₹50 crore / ₹250 crore₹125 crore / ₹500 crore

The portal also states that the investment and turnover data is pulled from government databases linked to PAN and GSTIN. That linkage is what makes pan-to-udyam possible in principle, and it is also why an enterprise whose PAN has no GSTIN can still hold a registration, because GSTIN is required only where the CGST Act applies to it.

Why the four identifiers do not replace each other

Think of them as four different registers kept by four different authorities for four different purposes. The tax department knows you as a taxpayer, the GST system knows you as a supplier of goods or services in a state, the Registrar of Companies knows you as a legal person created under company law, and the MSME ministry knows what size you declared. None of these registers is built to answer another register's question. A KYB design that respects this will ask each register only what it can answer, and will combine the answers on your side. That is the whole idea behind the waterfall in the later sections.

When do you need each identifier?

The right identifier depends on the decision you are making, not on what the customer happens to have on hand. Start from the decision.

Onboarding a supplier or vendor

You usually need a payable entity with a valid tax registration. GSTIN is the primary proof because invoices and input tax credit depend on it. Add CIN if the supplier is a company and your policy wants director names or incorporation status. Add Udyam only if you offer an MSME-specific benefit, such as preferred payment terms, or must tag MSME spend.

Onboarding a lending or fintech customer

Lenders generally want to know the legal form, who controls the entity and whether it is still active. For a company that means CIN first, then PAN and GSTIN for tax identity, then bank account verification with the bank account endpoint before disbursal. The KYC API use case for fintech and NBFC shows where business checks sit next to individual KYC.

Verifying a marketplace seller

Sellers range from proprietors to large companies. You cannot ask all of them for a CIN. Ask for a PAN and a GSTIN, check both, and request a CIN only when the PAN's holder category indicates a company.

Onboarding a B2B customer who will buy on credit

Here the risk is non-payment, so the decision wants more than existence. Confirm the entity is active (CIN status for companies, GSTIN status for others), note how long it has existed from incorporationDate, and compare the registered address with where you will deliver. These signals do not predict payment behaviour by themselves, so treat them as the identity layer beneath whatever credit assessment you run separately.

Re-verifying an existing counterparty

Registrations change status. A supplier active at onboarding may have a cancelled GSTIN a year later. Decide a re-check cadence by risk: high-value or regulated relationships more often, small recurring vendors less often. Each re-check is a billable call, so schedule it rather than running it on every invoice, and re-check immediately when a payment fails or an address changes.

When not to use these endpoints

Do not use a Udyam lookup to establish who someone is. Do not use a GSTIN lookup to establish incorporation. Do not treat any single positive result as a credit or fraud decision, and do not run these calls on individuals' PANs that you have no purpose and consent for. Your own legal and compliance team decides what a regulated KYB policy requires, and the StampMitra legal pages describe the service terms and acceptable use.

What does each StampMitra endpoint take and return?

All five endpoints below are real and documented in the product reference. They share an authentication scheme (Authorization: Bearer <key>, or the x-api-key header), a base URL of https://api-b2b.stampmitra.in, and a scope per endpoint.

ProductEndpointRequired scopeRequest fieldKey response fieldsPrice
GSTINPOST /v1/verify/gstinverification.gstin.readgstin (optional businessName)valid, legalName, tradeName, registrationStatus, principalPlaceOfBusiness₹5
CINPOST /v1/verify/cinverification.cin.readcinvalid, companyName, incorporationDate, cinStatus, companyStatus, directors[]₹5
UdyamPOST /v1/verify/udyamverification.udyam.readudyamNumberfound, udyamStatus, enterpriseName, enterpriseType, organizationType, certificateUrl₹5
PAN to GSTINPOST /v1/verify/pan-to-gstinverification.pan_to_gstin.readpanfound, gstins[] (gstin, state, status)₹5
PAN to UdyamPOST /v1/verify/pan-to-udyamverification.pan_to_udyam.readpanfound, status, udyamNumber₹5

Details that are easy to miss

Prices above are per call and are resolved from the product data; the live figure is on the pricing page. The PAN lookup used for holder verification is a separate product with its own scope, verification.pan.read, priced at ₹5.

Notice that CIN and the cross-lookups use the valid or found flag differently. GSTIN and CIN return valid. Udyam and both cross-lookups return found. Your code should branch on the right flag per endpoint instead of one shared check.

Each key carries scopes, so a key created for PAN only will receive a 403 on the GSTIN call. Request every scope your waterfall needs when you create the key, as described in the authentication docs.

The full reference for each product lives under verification docs, for example GSTIN, CIN and Udyam.

How do you set up a first call?

Before writing the waterfall, make one call per endpoint by hand so you see the real shapes.

Step 1: get a sandbox key

Sign up on the signup page with email, mobile and password. The quickstart says there is no credit card or manual approval and that you receive an sk_test_ key straight away. Sandbox keys are prefixed sk_test_, production keys sk_live_.

Step 2: confirm the scopes on the key

If a call returns 403, the key is valid but lacks the scope. The five scopes in the previous table, plus verification.pan.read if you verify PANs, are what a KYB integration needs.

Step 3: send a GSTIN call

cURL
curl -X POST https://api-b2b.stampmitra.in/v1/verify/gstin \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"gstin":"07AAAAA1111A1Z1","businessName":"Jane Doe Enterprises"}'

A positive response has this shape:

JSON
{
  "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 the capitalised GSTIN key in the response, while the request key is lowercase gstin. Read it exactly as returned.

Step 4: send a CIN call

cURL
curl -X POST https://api-b2b.stampmitra.in/v1/verify/cin \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"cin":"U74999DL2015PTC284009"}'
JSON
{
  "referenceId": "SMV-20260927-6R38Y3",
  "valid": true,
  "companyName": "JANE DOE ENTERPRISES PRIVATE LIMITED",
  "incorporationDate": "2015-03-12",
  "cinStatus": "ACTIVE",
  "companyStatus": "Active",
  "directors": [{ "din": "01234567", "name": "RAHUL KUMAR", "designation": "Director" }],
  "email": "[email protected]",
  "registrationNumber": "U74999DL2015PTC284009",
  "incorporationCountry": "India"
}

Step 5: send a Udyam call

cURL
curl -X POST https://api-b2b.stampmitra.in/v1/verify/udyam \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"udyamNumber":"UDYAM-MH-12-1234567"}'
JSON
{
  "referenceId": "SMV-20260927-5U29V7",
  "found": true,
  "udyamStatus": "Active",
  "enterpriseName": "Jane Doe Enterprises",
  "enterpriseType": "Proprietorship",
  "organizationType": "Micro",
  "certificateUrl": "https://udyamregistration.gov.in/Udyam_Download/UDYAM-MH-12-1234567.pdf"
}

What does the verification waterfall look like by entity type?

A waterfall is an ordered list of checks where each step either confirms a fact, stops the flow, or sends the case to manual review. Build one per entity type rather than one universal flow, because the available identifiers differ.

Entity typeStep 1Step 2Step 3Optional
Private or public companyCIN (existence, status, directors)PAN (holder and name)GSTIN (tax registration, address)Udyam, bank account
LLPPANGSTIN, or pan-to-gstinManual check of LLP records on MCAUdyam, bank account
ProprietorshipPAN of the proprietorGSTIN, or pan-to-gstinUdyam, or pan-to-udyamBank account
MSME claim (any form)Verify the underlying entity firstUdyam numberpan-to-udyam to cross-checkNone

Companies

Start at CIN, since a company that is struck off or inactive should stop the flow before you spend on anything else. Check valid, then cinStatus and companyStatus. Compare companyName with the name the customer declared. Then verify the company's PAN with the PAN endpoint and the GSTIN for the state in which it will invoice you. If the customer cannot supply the GSTIN, call pan-to-gstin and let them choose among the returned gstins.

LLPs

The CIN endpoint is documented for a Corporate Identification Number. An LLP is identified by an LLPIN, and the product reference documents no LLPIN lookup, so do not send an LLPIN to the CIN endpoint and do not claim LLP incorporation was verified by the API. For an LLP, verify the LLP's PAN and GSTIN through StampMitra, then confirm the LLP's registration on the MCA records yourself or through your counsel, and keep that outcome beside the referenceId values.

Proprietorships

A proprietorship has no separate legal identity and no CIN. Its identifiers are the proprietor's PAN, an optional GSTIN, and an optional Udyam registration. Verify the PAN first, because the GSTIN of a proprietor embeds the proprietor's own PAN. Then check the GSTIN's tradeName, which is often the name the customer actually uses. Our Udyam sample shows enterpriseType: "Proprietorship" for this reason.

MSME claims

Run Udyam last and only when the claim affects a decision. Confirm found, confirm udyamStatus is Active, and read organizationType for the size band. If the customer gave a PAN but not a Udyam number, use pan-to-udyam to find the registration yourself.

How do you combine calls and cross-lookups?

The cross-lookups exist for the common case where the customer remembers a PAN but not the other numbers. They also let you detect claims that do not hang together.

Using pan-to-gstin

cURL
curl -X POST https://api-b2b.stampmitra.in/v1/verify/pan-to-gstin \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"pan":"ABCDE1234F"}'
JSON
{
  "referenceId": "SMV-20260927-1L67Z5",
  "found": true,
  "gstins": [{ "gstin": "07AAAAA1111A1Z1", "state": "Delhi", "status": "Active" }]
}

The gstins array can hold more than one entry when the PAN has registrations in several states. Do not assume index zero is the right one. Match on state against the address the customer gave you, then run POST /v1/verify/gstin on the matching entry to fetch legal name and place of business.

Using pan-to-udyam

cURL
curl -X POST https://api-b2b.stampmitra.in/v1/verify/pan-to-udyam \
  -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"pan":"ABCDE1234F"}'
JSON
{ "referenceId": "SMV-20260927-0A44B2", "found": true, "status": "Active", "udyamNumber": "UDYAM-MH-12-1234567" }

The response gives a single udyamNumber and a status. To read the enterprise name and size, follow it with POST /v1/verify/udyam. Whether the lookup can return several registrations for one PAN is not stated in the product reference, so write your code to expect one value and log an alert if the shape ever changes.

Consistency checks across calls

Once you hold two or more responses, test them against each other. The PAN inside the GSTIN (characters three to twelve, per published GST guidance) should equal the PAN you verified. The enterpriseName from Udyam should resemble the legalName or tradeName from GSTIN. The state in the GSTIN should match the state in the CIN or the customer's stated address. A mismatch is not proof of fraud, since companies relocate and trade under other names, but it is exactly the signal to send to a person.

How do you handle name and data mismatches?

Real data disagrees with itself. A company is "Jane Doe Enterprises Private Limited" in the legal record, "Jane Doe Enterprises" on the GST trade name, and "JANE DOE ENTERPRISES PVT LTD" in a CIN response in capitals. If you treat a strict string comparison as the rule, you will reject good customers all day.

Normalise before comparing

Lowercase both strings, remove punctuation, collapse whitespace, and expand or drop common suffixes: pvt, private, ltd, limited, llp, & co. Compare the resulting tokens rather than the raw strings. This is your own logic, since the CIN and Udyam endpoints do not accept a name to match. Only GSTIN accepts an optional businessName, and PAN accepts an optional name that returns nameMatchResult.

Use the right field for each comparison

Compare the customer's declared legal name to legalName (GSTIN) or companyName (CIN). Compare a brand name to tradeName. Compare a proprietor's own name against the PAN registeredName. Do not compare a legal name to a trade name and call a difference a mismatch.

Three outcomes, not two

Design the decision as accept, review or reject.

SituationSuggested outcome
Identifier valid, active, normalised name equalAccept
Identifier valid, name differs only by suffix, spacing or trade versus legal nameAccept with a note
Valid but status not active, or name clearly different, or state differs from stated addressManual review
valid: false or found: false after a correctly formatted requestAsk the customer to re-check, then reject if the retry also fails

A worked mismatch example

A customer declares "Jane Doe Traders" and supplies the GSTIN from the sample above. The response returns legalName "Jane Doe Enterprises Private Limited" and tradeName "Jane Doe Enterprises". After normalisation the declared name shares one token ("jane doe") with the trade name but neither string equals the other. The right outcome is review, not reject: ask for the certificate of registration, or ask whether "Traders" is a division name. If the customer then supplies a CIN whose companyName matches the GSTIN legalName, the chain is consistent and the declared name was simply a brand. If the CIN names an entirely different company, you have found a real discrepancy worth escalating.

Keep the reason

Store which comparison failed, the two strings, and the referenceId of each call. A reviewer should see the evidence without re-running the call, because each re-run is a billable verification. The API errors and handling guide explains how to log request identifiers consistently.

How do you sequence calls to control cost?

Per-call pricing makes order a cost decision. The rates are on the pricing page, and each is resolved here from the product data: GSTIN ₹5, CIN ₹5, Udyam ₹5, PAN to GSTIN ₹5, PAN to Udyam ₹5, PAN ₹5. Check the live figures before budgeting.

Principles

First, validate formats locally. A malformed value fails with a 400 before any verification runs, but you still spent a network call and a customer's patience. Second, put the check most likely to fail earliest, so failed applications cost one call instead of four. For companies that is CIN, because a defunct company ends the flow. For sellers it is usually GSTIN. Third, do not run optional checks on applicants you will reject for other reasons. Fourth, cache a positive result for the length of time your policy allows and key it on the identifier, so a returning customer does not trigger a repeat call.

A cost-aware ordering

For a company: CIN, then PAN, then GSTIN, then Udyam only if the MSME claim matters. A rejected company costs one call at the CIN step. A fully verified company with an MSME claim costs four. For a proprietor: PAN, then GSTIN or pan-to-gstin when the number is missing, then Udyam if needed.

Cross-lookup versus asking the customer

A cross-lookup is a billable call. If a form field can capture the GSTIN with a format check, asking is free. Use pan-to-gstin when the customer genuinely does not know the number, or as an independent check that a supplied GSTIN is one of the PAN's registrations. Do not run it as a default on every applicant.

Billing behaviour to plan around

The product reference states that usage is recorded, and the wallet is debited for priced services, only after verification completes successfully, and that both positive and negative determinations are completed, billable verifications. Failed validation (400), authentication errors and upstream failures are not completed verifications. The reference is your authority here, and the billing and refund policy covers disputes.

How should the full flow look in Node.js?

The code below runs the company waterfall: CIN, then PAN, then GSTIN. It stops at the first hard failure, keeps every referenceId, and returns a decision of accept, review or reject. It uses the built-in fetch of current Node versions. The PAN response fields are from the PAN product sample.

JavaScript
const BASE = "https://api-b2b.stampmitra.in";
const KEY = process.env.STAMPMITRA_API_KEY; // sk_test_... in sandbox

async function verify(path, body) {
  const res = await fetch(`${BASE}/v1/verify/${path}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) {
    const e = json.error || {};
    const err = new Error(`${res.status} ${e.code}: ${e.message}`);
    err.status = res.status;
    err.requestId = e.requestId;
    throw err;
  }
  return json;
}

const norm = (s) =>
  s.toLowerCase().replace(/[^a-z0-9 ]/g, " ")
   .replace(/\b(pvt|private|ltd|limited)\b/g, " ")
   .replace(/\s+/g, " ").trim();

async function verifyCompany({ cin, pan, gstin, declaredName }) {
  const trail = [];
  const cinRes = await verify("cin", { cin });
  trail.push({ step: "cin", ref: cinRes.referenceId });
  if (!cinRes.valid) return { decision: "reject", reason: "CIN not valid", trail };
  if (cinRes.companyStatus !== "Active")
    return { decision: "review", reason: `Company status ${cinRes.companyStatus}`, trail };

  const panRes = await verify("pan", { pan, name: declaredName });
  trail.push({ step: "pan", ref: panRes.referenceId });
  if (!panRes.valid) return { decision: "reject", reason: "PAN not valid", trail };

  const gstRes = await verify("gstin", { gstin, businessName: declaredName });
  trail.push({ step: "gstin", ref: gstRes.referenceId });
  if (!gstRes.valid || gstRes.registrationStatus !== "Active")
    return { decision: "review", reason: "GSTIN not active", trail };

  const same = norm(cinRes.companyName) === norm(gstRes.legalName);
  return { decision: same ? "accept" : "review", reason: same ? "ok" : "name differs", trail };
}

Why the structure looks like this

Each helper throws on a non-2xx status and carries requestId, which you log and quote to support. Branching on valid happens only after a 2xx, because a 200 with valid: false is a legitimate answer and a 4xx or 5xx is a call problem. The audit trail array is what you persist next to the customer record. If you want a typed client instead of raw fetch, see the SDKs page.

How do you do the same in Python?

The Python version follows the same logic for a proprietorship: PAN, then GSTIN (found via pan-to-gstin if the customer did not give one), then optional Udyam through pan-to-udyam.

Python
import os
import requests

BASE = "https://api-b2b.stampmitra.in"
HEADERS = {
    "Authorization": f"Bearer {os.environ['STAMPMITRA_API_KEY']}",
    "Content-Type": "application/json",
}

class VerifyError(Exception):
    def __init__(self, status, code, message, request_id):
        super().__init__(f"{status} {code}: {message}")
        self.status, self.code, self.request_id = status, code, request_id

def verify(path, body):
    r = requests.post(f"{BASE}/v1/verify/{path}", json=body, headers=HEADERS, timeout=30)
    data = r.json()
    if not r.ok:
        e = data.get("error", {})
        raise VerifyError(r.status_code, e.get("code"), e.get("message"), e.get("requestId"))
    return data

def verify_proprietor(pan, gstin=None, want_udyam=False):
    trail = []
    p = verify("pan", {"pan": pan})
    trail.append(("pan", p["referenceId"]))
    if not p.get("valid"):
        return {"decision": "reject", "reason": "PAN not valid", "trail": trail}

    if gstin is None:
        x = verify("pan-to-gstin", {"pan": pan})
        trail.append(("pan-to-gstin", x["referenceId"]))
        active = [g for g in x.get("gstins", []) if g["status"] == "Active"]
        if not x.get("found") or not active:
            return {"decision": "review", "reason": "No active GSTIN", "trail": trail}
        gstin = active[0]["gstin"]  # confirm state with the customer if several

    g = verify("gstin", {"gstin": gstin})
    trail.append(("gstin", g["referenceId"]))
    if not g.get("valid") or g.get("registrationStatus") != "Active":
        return {"decision": "review", "reason": "GSTIN not active", "trail": trail}

    result = {"decision": "accept", "trade_name": g["tradeName"], "trail": trail}
    if want_udyam:
        u = verify("pan-to-udyam", {"pan": pan})
        trail.append(("pan-to-udyam", u["referenceId"]))
        if u.get("found"):
            d = verify("udyam", {"udyamNumber": u["udyamNumber"]})
            trail.append(("udyam", d["referenceId"]))
            result["size"] = d.get("organizationType")
    return result

The sample picks the first active GSTIN, which is only safe when the PAN has one registration. In production, match on state against what the customer declared, as described earlier.

What errors can you get, and how should you respond?

Every error from this API uses one normalised body, { "error": { "code", "message", "requestId" } }, and the product reference documents these statuses for the verification endpoints.

HTTP statuscodeMeaningWhat to do
400BAD_REQUESTInput failed format validation, e.g. "gstin must match the format 07AAAAA1111A1Z1"Fix the value; do not retry unchanged
401UNAUTHORIZEDMissing or invalid API keyCheck the header and the key environment
402PAYMENT_REQUIREDInsufficient wallet balance for this callTop up the wallet, then retry
403FORBIDDENValid key without the endpoint's scopeAdd the scope to the key
429TOO_MANY_REQUESTSExample message: "Rate limit exceeded: 60 requests per 60 seconds for this endpoint."Back off and slow your queue
502INTERNAL_ERRORUpstream verification service unreachableRetry with backoff; log the requestId

Format messages by product

The 400 message names the format. The CIN message reads "cin must match the format U74999DL2015PTC284009", the Udyam one "udyamNumber must match the format UDYAM-MH-12-1234567", and the PAN cross-lookups "pan must match the format ABCDE1234F". Use them to build your own client-side regexes so that bad input never leaves your server.

Retry rules

Retry 429 and 502 with exponential backoff and jitter, and cap attempts. Never retry 400, 401 or 403 automatically, because the same request will fail identically. Handle 402 as an operational alert, since it usually means your wallet needs attention rather than a bug. The referenceId is StampMitra's transaction id and is explicitly not an idempotency key, so a retry after a timeout can create a second transaction. When a request times out, check whether you actually received a response before assuming nothing was recorded. The broader pattern is in the errors and handling guide.

Negative results are not errors

A 200 with valid: false or found: false is a completed verification. Surface it to the user as "we could not confirm this number" and do not log it as an exception.

How do you test in the sandbox?

The pricing page states that sandbox calls run against sandbox data and never debit your wallet, and that sandbox is metered and audit-logged. The authentication page says keys are environment-scoped. Use that to test the entire waterfall for free.

What to test

Write a test for each decision branch, not only the happy path: an accepted company, a CIN that returns not valid, a company with a non-active status, a GSTIN whose name differs from the declared one, a proprietor with no GSTIN who falls through to pan-to-gstin, and a 400 from a malformed value. Use the sample identifiers from the product reference (07AAAAA1111A1Z1, U74999DL2015PTC284009, UDYAM-MH-12-1234567, PAN ABCDE1234F), because they are shown to be well formed. Which sample values return negative or error states in sandbox is not documented, so do not rely on guessing. For negative branches, stub the HTTP layer in your unit tests with the response shapes from this guide.

What sandbox cannot tell you

Sandbox data is not your customers' data. It will not tell you how often real GSTINs come back inactive, how your name-matching behaves on real legal names, or how your queue behaves at production latency. Plan a small, supervised production pilot for that. The sandbox versus production keys guide goes through the differences, and the Postman collection gives you ready requests for manual exploration.

Testing the cost logic

Count calls per scenario in your tests. Assert that a rejected company makes exactly one outbound call, that a fully verified company makes the number you expect, and that a customer who supplies a GSTIN never triggers pan-to-gstin. These assertions catch accidental cost regressions long before a bill does, and they cost nothing in sandbox.

Contract tests

Pin the field names you read (valid, found, registrationStatus, companyStatus, gstins, udyamNumber) in a test against a recorded response. If the response shape ever changes, you want a failing test rather than a silent undefined that your logic reads as "not active".

What should you do before going to production?

Moving from sk_test_ to sk_live_ is a business step as well as a technical one. The authentication docs state that production keys require a KYC-approved organisation, and the pricing page says you apply for production access from the dashboard and create a production key once approved.

Technical checklist

Store the key in a secret manager and never in client-side code or a repository. Scope the key to only the verification endpoints you use. The authentication docs note that sensitive scopes can additionally be restricted to an IP allowlist, so use it where it is offered. Fund the wallet and add an alert on 402 so a low balance does not silently break onboarding. Add rate limiting on your own side below the documented limit shown in the 429 example. Log referenceId and requestId on every call.

Data protection checklist

Business verification still touches personal data: directors' names and DINs in a CIN response, a proprietor's name behind a PAN. General good practice under the Digital Personal Data Protection Act, 2023 is to collect only what the purpose needs, tell the person why, and not keep it longer than needed. I have not verified the Act's section-level text for this guide, so have your counsel confirm obligations that apply to you. Practically, that means:

  • Collect identifiers for a stated verification purpose and record the basis for it.
  • Store the decision, the referenceId and the minimal fields you need. Avoid copying the full director list into every downstream system.
  • Restrict who can read stored responses and set a retention period.
  • Do not run lookups on identifiers you have no business reason to check.

StampMitra publishes its developer privacy policy, API security policy, data processing addendum and acceptable use policy. Read them before you design retention. If you onboard other businesses as your own customers, see also the developer onboarding verification policy.

What goes wrong most often?

These are the failure causes that account for most support questions on business verification flows.

Treating GSTIN as proof of incorporation

A GSTIN proves GST registration. A company, an LLP and a sole proprietor all hold them. Without the CIN step you have not established that a company exists.

Sending the wrong identifier to the wrong endpoint

An LLPIN sent to /v1/verify/cin is not a documented use. A PAN sent to /v1/verify/gstin will fail the 15-character format check with a 400. Route by identifier type on your side, using regexes that match the format messages.

Trusting the first GSTIN in a list

pan-to-gstin can return several registrations. Picking index zero attaches the wrong state's address to the customer.

Strict name equality

Capitals, "Pvt Ltd" versus "Private Limited", and trade versus legal names produce false mismatches. Normalise, and route the rest to review.

Ignoring status fields

valid: true with registrationStatus not Active, or companyStatus not Active, is not a clean pass. Decide explicitly what each status value means for your policy.

Hardcoding MSME thresholds

The Udyam portal shows limits that changed on 1 April 2025 (for example micro up to ₹2.5 crore investment and ₹10 crore turnover on the portal's table). Read organizationType from the response and never recompute the category yourself.

Retrying without thinking about cost

Because the referenceId is not an idempotency key and completed negative results are billable, a naive retry loop on a business rule failure burns money. Retry only transport errors and the documented retryable statuses.

Forgetting that Udyam is self-declared

The Udyam portal describes registration as based on self-declaration. A found and active Udyam number tells you a registration exists, not that the figures were audited.

How do you troubleshoot a business verification that looks wrong?

Work from the outside in: request, then key, then data, then policy.

The call returns 400 but the number looks right

Check for whitespace, lowercase letters, or a copied hyphen or zero-width character. Trim and upper-case before sending. For Udyam, the format shown in the reference has hyphens, UDYAM-MH-12-1234567. Send it in that shape.

The call returns 403 on one endpoint only

The key lacks that endpoint's scope. Compare the endpoint's scope in the table above with the key's scopes in your dashboard, and create or update the key.

A valid-looking GSTIN comes back valid: false

Make sure you did not transpose characters, and try the check character logic client-side if you have it. Then ask the customer to confirm the number from their registration certificate. A genuine negative result can also mean the registration was cancelled. Treat it as "unable to confirm" and request fresh documents rather than an accusation.

Names never match

Print the two normalised strings. The usual cause is the wrong comparison pair (legal name against trade name), or suffix handling. Adjust normalisation before loosening thresholds.

Everything works in sandbox but fails live

Look first at 401 (a sk_test_ key still configured), then 403 (the live key was created with fewer scopes), then 402 (empty wallet), then IP allowlisting if you enabled it.

Intermittent 502

This is the documented upstream-unreachable response. Back off and retry, and send the requestId to [email protected] if it persists. The support page and contact page list other routes.

How StampMitra Helps

StampMitra is a private LegalTech platform operated by Bani Global Industries LLP. It is not a government agency and not a law firm, and nothing here is legal advice. What it provides is a single API surface for the business checks described above, so one integration, one key model and one error format cover GSTIN, CIN, Udyam, PAN and the two PAN cross-lookups.

Concretely, today the StampMitra verification API gives you five business-oriented endpoints (gstin, cin, udyam, pan-to-gstin, pan-to-udyam) plus pan and bank-account for completing a KYB record. Authentication is a bearer key with per-endpoint scopes, with sk_test_ keys for a free sandbox and sk_live_ keys after production approval. Every response carries a referenceId you can store as an audit reference, and every error uses the same error.code, message and requestId shape. Pricing is per successful call, deducted from a prepaid wallet, and the pricing page shows the live rates. You can browse every field in the verification docs, start from the quickstart, or use the MCP server and Build with AI pages if you want an assistant to scaffold the integration. The webhooks page covers event delivery for flows that are not strictly request and response.

It is worth being plain about the limits. The API reflects the five documented endpoints and does not currently offer an LLPIN lookup, a combined one-call "verify this business" endpoint, or automated name matching on CIN and Udyam. The decision logic in this guide, including name normalisation, thresholds and the accept, review or reject policy, is yours to build and own, and your legal and compliance advisers should confirm what your regulated process needs. The same applies to your data protection duties as the party that decides why and how the data is used.

As for what is next, StampMitra is planning to keep widening the verification catalogue and the developer tooling around it, but those are plans and not commitments, and this guide only describes what exists today. To try the flow, sign up for a sandbox key, run the three cURL calls above, and then read the sibling guides on GSTIN verification and sandbox versus production keys before you apply for production.

API reference at a glance

Company CIN: paramsPOST /v1/verify/cin

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
cinrequiredstringCorporate Identification Number.
Company CIN: responsePOST /v1/verify/cin
{
  "referenceId": "SMV-20260927-6R38Y3",
  "valid": true,
  "companyName": "JANE DOE ENTERPRISES PRIVATE LIMITED",
  "incorporationDate": "2015-03-12",
  "cinStatus": "ACTIVE",
  "companyStatus": "Active",
  "directors": [
    {
      "din": "01234567",
      "name": "RAHUL KUMAR",
      "designation": "Director"
    }
  ],
  "email": "[email protected]",
  "registrationNumber": "U74999DL2015PTC284009",
  "incorporationCountry": "India"
}
Company CIN: pricePOST /v1/verify/cin

Company CIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).

Udyam Registration: paramsPOST /v1/verify/udyam

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
udyamNumberrequiredstringUdyam Registration Number (URN).
Udyam Registration: responsePOST /v1/verify/udyam
{
  "referenceId": "SMV-20260927-5U29V7",
  "found": true,
  "udyamStatus": "Active",
  "enterpriseName": "Jane Doe Enterprises",
  "enterpriseType": "Proprietorship",
  "organizationType": "Micro",
  "certificateUrl": "https://udyamregistration.gov.in/Udyam_Download/UDYAM-MH-12-1234567.pdf"
}
Udyam Registration: pricePOST /v1/verify/udyam

Udyam Registration: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).

GSTIN: paramsPOST /v1/verify/gstin

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
gstinrequiredstring15-character GSTIN.
businessNamestringBusiness name to match.
GSTIN: responsePOST /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"
}
GSTIN: pricePOST /v1/verify/gstin

GSTIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).

Frequently asked questions

What is the difference between GSTIN, CIN and Udyam?

A GSTIN proves a GST registration in one state, a CIN proves a company is registered with the Registrar of Companies, and a Udyam number records a self-declared MSME registration. They answer different questions and do not replace each other.

Can I verify a business in India with just a GSTIN?

A GSTIN confirms a GST registration, its status, legal and trade name and principal place of business. It does not prove incorporation, directors or MSME status, so companies should also be checked by CIN.

Which StampMitra endpoint verifies a company's CIN?

Send a POST request to /v1/verify/cin with the cin field and a key that has the verification.cin.read scope. The response includes valid, companyName, incorporationDate, cinStatus, companyStatus and a directors array.

How do I find a business's GSTIN from its PAN?

Call POST /v1/verify/pan-to-gstin with the pan field. The response returns found and a gstins array with gstin, state and status for each registration, so choose the entry matching the customer's state.

Does an LLP have a CIN that I can verify through the API?

LLPs are identified by an LLPIN, not a CIN, and the StampMitra product reference documents no LLPIN lookup. For an LLP, verify the PAN and GSTIN through the API and confirm LLP registration on MCA records separately.

Is a negative verification result billable?

According to the product reference, both positive and negative determinations, such as valid false or found false, are completed verifications that are billable. A 400 validation error is rejected before verification and is not a completed check.

How should I handle a company name that does not match the GSTIN legal name?

Normalise case, punctuation and suffixes like Pvt and Ltd, then compare legal name to legal name and brand to trade name. Route remaining differences to manual review instead of rejecting automatically.

Can I test business verification without paying?

Yes. Sandbox keys starting with sk_test_ run against sandbox data and never debit your wallet, so you can test the whole waterfall for free before applying for production access and an sk_live_ key.

What is the Udyam verification response used for?

It returns found, udyamStatus, enterpriseName, enterpriseType, organizationType and certificateUrl. Use it to confirm an MSME claim and size category, remembering that Udyam registration is self-declared.

Sources

All sources were checked on 2026-10-16. Where an official page could not be opened, that is stated.

  • Udyam Registration portal, Ministry of MSME, "Important" page: https://udyamregistration.gov.in/Important.aspx (opened and read 2026-10-16; used for the classification limits effective 1 April 2025, the self-declared, paperless and free registration description, and the Aadhaar, PAN and GSTIN requirements).
  • StampMitra product reference for the gstin, cin, udyam, pan-to-gstin, pan-to-udyam, pan and bank-account verification products, the authentication docs, quickstart and pricing pages on this site (read 2026-10-16; the source for every path, scope, field, error body, sandbox and billing statement).
  • GST portal, gst.gov.in: the site was reachable but its pages did not expose GSTIN structure text on 2026-10-16. The GSTIN structure is therefore described from published GST explainers and flagged for human review.
  • Ministry of Corporate Affairs, mca.gov.in: the site returned HTTP 403 to automated access on 2026-10-16, so CIN structure is described from secondary explainers and flagged for human review. Confirm the segment meanings on MCA.
  • Income Tax Department e-filing portal, incometax.gov.in: the page opened did not list PAN holder-category codes on 2026-10-16, so the PAN holder-category statement is kept general. The Digital Personal Data Protection Act, 2023 text was not opened for this guide.