A vendor onboarding flow should run its checks in order of cost and decisiveness: free offline validation and internal duplicate checks first, then the PAN, then the GSTIN (compared with the PAN), then the CIN for companies, then the bank account, and finally documents and a human review for anything the automated steps could not settle. Stop at the first hard failure, because every later check on a vendor that has already failed is money spent on a decision you have already made.
The order is not arbitrary. Each step answers a question that later steps depend on, and each later step costs more than the one before. Put the steps in the wrong order and you pay a bank to verify an account for a vendor whose PAN does not exist, or you approve a vendor on a registration that belongs to somebody else.
This guide lays out the whole flow for a company that buys from vendors and pays them: what to collect, what each check protects against, the state machine that holds it together, code for an orchestrator in Node.js and Python, duplicate detection before you spend any money, what the vendor sees at every step, how to keep the vendor base clean after approval, and how to backfill vendors you already have. It uses the StampMitra verification endpoints throughout and says plainly where a check needs a human or a document.
Key takeaways
- Order the checks by cost and decisiveness: offline validation, internal duplicate check, PAN, GSTIN with PAN comparison, CIN for companies, bank account with name match, then documents and review.
- Stop at the first hard failure. Do not call a later, dearer endpoint for a vendor that has already failed an earlier one.
- Check for duplicates in your own data before any API call. A duplicate PAN, GSTIN or bank account is often the most valuable signal and costs nothing.
- Treat the PAN as the anchor. The GSTIN must embed it, the bank account name must match the verified legal name, and the CIN company name must agree.
- Model the flow as a state machine with persisted results, so a crash or a retry never double-bills or loses progress.
- Never retry a bank account verification blindly. It involves a bank transfer and the reference documents no idempotency for verification endpoints.
- A changed bank account on an existing vendor is the classic fraud event. Re-run the account check and add a hold or a second approval.
- Re-check registrations on a schedule, and before large payments. Statuses change after onboarding.
- Verification narrows risk. It does not make a vendor trustworthy, and it does not replace contracts, due diligence on the relationship or your own policy.
What a vendor flow protects you from
Be specific about the failures you are trying to prevent. Each maps to a check, and a check you cannot tie to a risk is a candidate for removal.
| Risk | What it looks like | Which check addresses it |
|---|---|---|
| A vendor that does not exist | A fabricated name, an invented tax number | PAN, GSTIN, CIN lookups |
| A real number belonging to somebody else | A genuine, active GSTIN quoted by an impostor | GSTIN embedded PAN against verified PAN, name comparison |
| A cancelled or suspended GST registration | Invoices that cannot support input tax credit | GSTIN registrationStatus and re-checks |
| A duplicate vendor | The same business registered twice under two names, to split approvals or hide spend | Internal duplicate check on PAN, GSTIN, bank account |
| A payment to the wrong account | A typo or a deliberately substituted account | Bank account verification with name match |
| A changed account at the last minute | A fraudster emailing "our bank details have changed" | Re-verification and a hold on any account change |
| A company with no authority to contract through the applicant | A person with no standing signs for the company | CIN directors, authorisation document |
| A conflict of interest | A vendor owned by an employee | Not detectable through these APIs. A policy and declaration matter |
The last row is deliberate. Verification APIs check records. They do not read intent or relationships. A vendor flow that treats them as the whole answer creates false comfort.
The order of checks, in one table
| # | Step | What it costs | What it settles | Stop if |
|---|---|---|---|---|
| 0 | Collect inputs by vendor type | Nothing | What you can check | The vendor cannot supply what its type must hold |
| 1 | Offline validation | Nothing | Typos, impossible identifiers | Format, state code or check character fails |
| 2 | Internal duplicate check | Nothing | Is this vendor already in your system | A duplicate on a hard identifier |
| 3 | PAN verification | One call | The tax identity exists with a name | valid is false |
| 4 | GSTIN verification | One call | A GST registration exists, is active, and embeds the verified PAN | Not found, PAN mismatch, a non-active status |
| 5 | CIN verification (companies) | One call | The company exists, is active, and who its directors are | Not found, inactive |
| 6 | Bank account verification | One call, involves a bank transfer | The account exists and its holder name matches | Not valid, name far from the verified name |
| 7 | Documents and human review | Staff time | Authority, anything the lookups could not settle | The reviewer rejects |
| 8 | Activation controls | Policy | A safe first payment | The hold is not released |
The cost column explains the order. Steps one and two are free, so they run first. Steps three to five are single lookups of different cost and different decisiveness. Step six involves a bank, which makes it both the most expensive and the one you least want to repeat, so it comes last among the automated checks.
Why not run everything in parallel?
Parallel calls are faster, and for an interactive flow that sounds attractive. The trade is cost and cleanliness. If the PAN fails, a parallel run has already paid for a GSTIN lookup and a bank transfer that you did not need. Worse, a parallel bank check on a vendor whose PAN you have not yet verified has no verified name to compare against, so you are forced to use the typed name, which is the weakest candidate.
A sequential flow is rarely slow enough to matter. Each StampMitra verification is a single synchronous call, so a full company path of four calls is four round trips in sequence, not a long wait. Measure it on your own network before deciding that parallelism is needed. Where latency matters more than cost, you can run the PAN, GSTIN and CIN lookups in parallel, since they are independent and cheap, and keep the bank account check strictly after them. Do not parallelise across the bank check.
Three vendors, one morning
Here is how the order plays out for three fictional vendors submitted on the same morning.
Vendor A, a freelance designer. Submits a PAN and a bank account. Offline checks pass. No duplicates. The PAN lookup returns valid: true with a name that agrees with the typed name after normalisation. The bank check returns a valid account and a score well above the review band. Two paid calls, no human, state VERIFIED, then ACTIVE with a first-payment hold.
Vendor B, a trading company. Submits a company PAN, a GSTIN, a CIN and a bank account. Offline checks pass and no duplicate exists. The PAN is valid and the fourth letter is C. The GSTIN lookup returns Active, with the embedded PAN equal to the verified PAN, but the legalName differs from the PAN's registered name: "Pvt Ltd" against "Private Limited" resolves after normalisation, and what remains is a difference in one word that looks like a recent rename. The decision is REVIEW with NAME_DIFFERS. The flow does not stop. The CIN check runs and shows an active company whose name matches the GSTIN rather than the PAN, which supports the rename theory. The bank check passes. A reviewer sees four results and one reason, asks the vendor for the certificate of incorporation showing the name change, receives it within the day and approves. Four paid calls, one human decision.
Vendor C, a supplier with a GSTIN that is not theirs. Submits a company PAN, a GSTIN and a bank account. Offline checks pass. The duplicate check finds nothing. The PAN is valid. The GSTIN lookup returns Active, but characters 3 to 12 are a different PAN than the one just verified. Decision: FAIL with PAN_MISMATCH. The flow ends. No CIN call, no bank call, and therefore no bank transfer to an account that belongs to a stranger. The vendor receives a neutral message asking which PAN the registration was made under. Two paid calls instead of four, and a mismatch caught that a name check alone could have missed.
The three paths use the same code. The flow differs because the data differs, and the order of checks decides how much each path costs.
Step 0: collect the right inputs for each vendor type
The first decision is what to ask for. Asking every vendor for every identifier causes drop-off and pays for lookups that cannot succeed. Ask for what the vendor type can hold.
| Vendor type | Identifiers | Notes |
|---|---|---|
| Individual or freelancer | PAN, bank account | No GST registration if below the threshold or exempt |
| Sole proprietor | Proprietor's PAN, GSTIN if registered, bank account | Account may be in the proprietor's or the business name |
| Partnership firm | Firm PAN, GSTIN if registered, bank account | PAN's fourth letter should be F |
| LLP | Firm PAN, LLPIN, GSTIN if registered, bank account | Check what the CIN endpoint accepts for LLPs on your account |
| Company | Company PAN, GSTIN, CIN, bank account | Three independent records to cross-check |
| Trust or society | Trust PAN, GSTIN if registered, bank account | PAN fourth letter T or A |
| Foreign vendor | None of these | Route to a different process |
Alongside the identifiers collect: the legal name exactly as on the PAN, the trade name if different, the registered address, a contact person and email, the vendor's own declaration of any relationship with your staff, and, for the bank account, the holder name as it appears with the bank. Ask for a cancelled cheque or bank letter as a document if your policy uses documents as the backstop.
One design point: collect the vendor type first, and let it decide which fields to show. A vendor who selects "company" sees CIN. A vendor who selects "freelancer" does not. This is how the StampMitra onboarding form itself is built.
Step 1: offline validation
Run these on your server before anything else. They are free and catch typos.
- PAN shape: five letters, four digits, one letter; the fourth letter must agree with the claimed type.
- GSTIN shape and state code; the check character on production keys only.
- CIN shape (loosely, since real CINs have variations).
- Account number: nine to eighteen digits. IFSC: four letters, a zero, six letters or digits.
The code for each is in PAN vs GSTIN vs CIN and How to verify a GSTIN programmatically. A validation failure here is a typo, so show the vendor a correction message and do not move the application forward.
Step 2: duplicate detection before you spend anything
Before any paid call, look in your own data. A vendor that already exists under the same PAN, GSTIN or bank account is the cheapest and most valuable signal in the whole flow.
-- Which existing vendors share any hard identifier with the applicant?
SELECT id, legal_name, status, 'pan' AS matched_on FROM vendors WHERE pan = :pan
UNION
SELECT id, legal_name, status, 'gstin' FROM vendors WHERE gstin = :gstin
UNION
SELECT v.id, v.legal_name, v.status, 'bank_account'
FROM vendors v JOIN vendor_accounts a ON a.vendor_id = v.id
WHERE a.account_number_hash = :account_hash AND a.ifsc = :ifsc;What the hits mean differs by identifier.
| Matched on | Typical meaning | Action |
|---|---|---|
| PAN | The same legal entity, possibly with a second GSTIN for another state | Link to the existing vendor and add a registration, rather than creating a second vendor |
| GSTIN | The same registration | Treat as a duplicate and stop |
| Bank account | Two vendors paid into one account | Route to review. Legitimate in a group of companies, suspicious elsewhere |
Store the bank account as a salted hash for matching and keep only the last four digits for display, so that duplicate detection does not require plaintext account numbers in more places than necessary. Names are weak duplicate evidence on their own, because "Sharma Traders" is a common name. Use them only to suggest, never to decide.
Step 3: verify the PAN
The PAN is the anchor of the rest of the flow. Call POST /v1/verify/pan with the PAN and the legal name as the vendor entered it.
curl -X POST "https://api-b2b.stampmitra.in/v1/verify/pan" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pan":"ABCDE1234F","name":"Jane Doe Enterprises Private Limited"}'Branch on valid, then panStatus, then the name result and score, then type against the claimed vendor type. A valid: false stops the flow. A non-VALID status or a weak name match goes to review. The decision function, the name normaliser and the reasons for sending different names to a human rather than rejecting them are in how to verify a PAN and match the name. What the PAN call does and does not prove is in the PAN verification guide.
Keep the registeredName from this step in memory for the rest of the flow. It becomes the reference name that the bank account is compared against.
Step 4: verify the GSTIN, and compare it with the PAN
For a vendor with a GSTIN, call POST /v1/verify/gstin, then run three comparisons in code.
registrationStatusmust beActive. Anything else goes to review, and a cancelled or suspended registration should usually stop the vendor from being set up for GST invoicing.- Characters 3 to 12 of the GSTIN must equal the verified PAN. A mismatch is a hard failure.
legalNamemust agree with the PAN's registered name after normalisation. A difference goes to review.
If the vendor has only a PAN and says it is GST registered, use the PAN to GSTIN lookup to list its registrations and ask which one will appear on invoices. Never accept the first GSTIN in the list as the right one without confirming. The status values, what they mean and what the response does not carry are in How to verify a GSTIN programmatically.
For a vendor with no GSTIN, record why (below the threshold, exempt supplier, service provider not registered). Whether you may buy from an unregistered supplier, and how to treat the tax on those purchases, is a question for your finance and tax team. The API cannot answer it.
Step 5: verify the CIN, for companies
For a company vendor, call POST /v1/verify/cin. Check that the company is active in both cinStatus and companyStatus, compare companyName with the PAN and GSTIN names, and read the directors list. Then compare the person who applied with the directors.
If the applicant is a listed director, your policy may waive the heavier authority document, which is what StampMitra's own production access form does with the board resolution. If the applicant is not a director, ask for an authorisation letter or board resolution signed on the company's behalf. The lookup shows who the directors are. It does not show who the company has authorised.
Udyam registration, if your policy treats it as relevant for small vendors, has its own lookup; the guide on GSTIN, CIN and Udyam business verification covers it. For the broader question of what you must check for a business as against an individual, see KYC vs KYB.
Step 6: verify the bank account, last
Only vendors who passed everything above reach this step. Call POST /v1/verify/bank-account with the account number, the IFSC and, as name, the verified legal name from the PAN lookup (or the proprietor's name, depending on your policy for proprietorships). Branch on valid, then accountStatus, then the score.
Three rules, all explained in penny drop vs name match:
- Compare the bank name with the verified name, not with what the vendor typed.
- Route partial matches to review. Bank names are truncated, abbreviated and reordered.
- Do not retry a timed-out call blindly. The call involves a bank transfer and a retry can send a second credit. Check the logs for a completed call first.
Bank account verification is flagged as higher risk in StampMitra's production access form, which means it opens after a short extra check once your application is approved. Plan for it when you apply.
Step 7: documents and human review
Some vendors will reach this step with everything green. Others will arrive with reasons. The reviewer's screen should show: the vendor and its type; every verification result with its referenceId and time; the reasons that put the case in the queue; the documents uploaded; and the relationships derived from lookups (directors) and documents (authority).
Documents that commonly help: a cancelled cheque or bank letter for a bank-name difference; a registration certificate for a status question; an incorporation certificate for a company-name difference; an authorisation letter for a signatory who is not a director. Which documents you accept is a policy decision for your compliance owner.
Give the reviewer a closed list of actions (approve, reject, request a document, request a corrected number, escalate) and require one line of reasoning for each decision. A closed list lets you report on outcomes later, and the reasoning is what you show an auditor.
Step 8: activation and first-payment controls
Approval should not mean "pay immediately". A few cheap controls reduce the damage if something slipped through.
- A first-payment hold. Delay the first payment to a new vendor by a period your policy sets, and require a second approver above a threshold.
- Limits. A new vendor starts with a cap on single payments and on the monthly total, raised after a history builds up.
- A change-of-account rule. Any change to a vendor's bank account triggers a fresh bank account verification, a hold, and a callback to a known contact through a channel the vendor did not supply in the change request.
- A named owner. Every vendor has an internal owner who is responsible for the relationship, so that a flag has somewhere to go.
None of these are API features. They are the policy that turns verification results into safer payments.
The state machine
Persist the flow as explicit states, so that restarts, retries and reviews are well defined.
| State | Meaning | Moves to |
|---|---|---|
DRAFT | Vendor is filling the form | SUBMITTED |
SUBMITTED | Inputs received | INVALID_INPUT, DUPLICATE, CHECKING |
INVALID_INPUT | An offline check failed | SUBMITTED after correction |
DUPLICATE | A hard identifier matches an existing vendor | IN_REVIEW, CLOSED |
CHECKING | Paid checks are running or queued | IN_REVIEW, REJECTED, VERIFIED |
IN_REVIEW | A person is deciding | VERIFIED, REJECTED, NEEDS_INFO |
NEEDS_INFO | Waiting for a vendor document or correction | CHECKING, CLOSED |
VERIFIED | All required checks passed | ACTIVE |
ACTIVE | Payable, with limits and a hold | SUSPENDED, RECHECK_DUE |
RECHECK_DUE | A scheduled re-check is due | ACTIVE, IN_REVIEW |
SUSPENDED | Payments blocked | IN_REVIEW, CLOSED |
REJECTED, CLOSED | End states | none |
Store each paid check as its own row: kind, input hash, referenceId, outcome, reasons and timestamp. The orchestrator reads the rows to know what is done, so a restart resumes instead of repeating.
An orchestrator in Node.js
// onboard.mjs
import { verifyPan } from "./pan-client.mjs";
import { verifyGstin } from "./gstin-client.mjs";
import { verifyCin } from "./cin-client.mjs";
import { verifyBank } from "./bank-client.mjs";
import { decidePan } from "./pan-decision.mjs";
import { decideGstin } from "./gstin-decision.mjs";
import { decideBank } from "./bank-decision.mjs";
export async function runChecks(vendor, store) {
const done = await store.checksFor(vendor.id); // map of kind -> stored result
async function step(kind, fn) {
if (done[kind]) return done[kind]; // resume, never repeat a paid call
const result = await fn();
await store.saveCheck(vendor.id, kind, result);
done[kind] = result;
return result;
}
const pan = await step("pan", async () => {
const res = await verifyPan({ pan: vendor.pan, name: vendor.legalName });
return { ...decidePan(res, vendor.legalName), res };
});
if (pan.decision === "FAIL") return finish(vendor, store, "REJECTED", pan);
const verifiedName = pan.res.registeredName;
if (vendor.gstin) {
const g = await step("gstin", async () => {
const res = await verifyGstin({ gstin: vendor.gstin });
return { ...decideGstin(res, { expectedPan: vendor.pan, expectedName: verifiedName }), res };
});
if (g.decision === "FAIL") return finish(vendor, store, "REJECTED", g);
}
if (vendor.type === "COMPANY") {
const c = await step("cin", async () => {
const res = await verifyCin({ cin: vendor.cin });
const active = res.valid && /^active$/i.test(res.cinStatus ?? "") && /^active$/i.test(res.companyStatus ?? "");
const applicantIsDirector = (res.directors ?? []).some((d) => d.name?.toUpperCase() === vendor.applicantName?.toUpperCase());
return { decision: active ? "PASS" : "FAIL", reasons: active ? [] : ["COMPANY_NOT_FOUND_OR_INACTIVE"], applicantIsDirector, res };
});
if (c.decision === "FAIL") return finish(vendor, store, "REJECTED", c);
}
const bank = await step("bank", async () => {
const res = await verifyBank({ accountNumber: vendor.account.number, ifsc: vendor.account.ifsc, name: verifiedName });
return { ...decideBank(res), res };
});
if (bank.decision === "FAIL") return finish(vendor, store, "REJECTED", bank);
const needsReview = Object.values(done).some((c) => c.decision === "REVIEW");
return finish(vendor, store, needsReview ? "IN_REVIEW" : "VERIFIED", null);
}
async function finish(vendor, store, state, because) {
await store.setState(vendor.id, state, because?.reasons ?? []);
return { state, reasons: because?.reasons ?? [] };
}Three design choices. step makes every paid call resumable: a stored result is returned instead of calling again. The loop stops at the first FAIL, so a vendor that fails the PAN never costs a bank call. And the verified name from the PAN step, not what the vendor typed, is the name the bank result is judged against.
The same flow in Python
def run_checks(vendor, store):
done = store.checks_for(vendor["id"])
def step(kind, fn):
if kind in done:
return done[kind]
result = fn()
store.save_check(vendor["id"], kind, result)
done[kind] = result
return result
pan = step("pan", lambda: {**decide_pan(res := verify_pan(vendor["pan"], vendor["legal_name"]), vendor["legal_name"]), "res": res})
if pan["decision"] == "FAIL":
return finish(vendor, store, "REJECTED", pan)
verified_name = pan["res"]["registeredName"]
if vendor.get("gstin"):
g = step("gstin", lambda: {**decide_gstin(res := verify_gstin(vendor["gstin"]),
expected_pan=vendor["pan"], expected_name=verified_name), "res": res})
if g["decision"] == "FAIL":
return finish(vendor, store, "REJECTED", g)
bank = step("bank", lambda: {**decide_bank(res := verify_bank(vendor["account"], verified_name)), "res": res})
if bank["decision"] == "FAIL":
return finish(vendor, store, "REJECTED", bank)
needs_review = any(c["decision"] == "REVIEW" for c in done.values())
return finish(vendor, store, "IN_REVIEW" if needs_review else "VERIFIED", None)The functions verify_pan, verify_gstin, verify_bank, decide_pan, decide_gstin and decide_bank are the ones built in the endpoint guides linked above. Keep them in one module so the Node and Python services behave alike.
What the vendor sees at each step
The vendor's experience decides how many finish. Every state should have a message that says what is happening and what, if anything, they must do.
| State | What the vendor sees |
|---|---|
INVALID_INPUT | The exact field and a plain instruction: "The GSTIN should have 15 letters and digits. Please check what you typed." |
CHECKING | "We are checking your details. We will email you as soon as we have a result." |
NEEDS_INFO | A specific request and an upload button: "Please upload a cancelled cheque so we can confirm your account name." |
IN_REVIEW | "Our team is reviewing your details. We will email you within the time your procurement team has set." |
VERIFIED and ACTIVE | A confirmation, the vendor's code and what happens next |
REJECTED | A neutral, non-accusing message with a contact for questions. Never say "fraud" or "fake" |
| Service trouble (timeout, 502) | "We could not complete a check right now. We will try again and email you." Do not show a failure for something that is not the vendor's fault |
Two further practices. Send an email at each state change, with the vendor code in the subject, so a vendor can quote it. And give procurement staff a status page showing where every vendor is and how long each has been there, because a vendor stuck in review for a week is a vendor who goes to a competitor.
Keeping the vendor base clean after approval
Approval is the start of a lifecycle.
- Scheduled re-checks of registrations. GST registrations change status. Re-check by risk: high-spend vendors before payment runs above a threshold and monthly, regular vendors quarterly, occasional ones on next use after a year. Each re-check is a billable call with its own
referenceId. - Event-driven checks. A bank account change, a change of authorised signatory, a name change request, a very large invoice, a vendor complaint. Each can trigger a targeted re-check.
- Flags and owners. A status change raises a flag against the vendor's internal owner, with a deadline.
- Dormant vendors. Vendors with no activity for a long period should be marked dormant and required to re-verify before the next payment.
- Exit. When a vendor is closed, keep its verification record for the retention period your policy sets and block further payments.
Backfilling vendors you already have
Most teams introducing this flow already have hundreds or thousands of vendors with no verification record. Do not run them all at once.
- Segment by risk. Rank vendors by annual spend, by payment frequency and by whether they are paid into a bank account you have never verified. Start with the top of the list.
- Run the free checks first. Offline validation and duplicate detection across the whole base cost nothing and usually surface surprises: malformed numbers and the same account used by several vendors.
- Batch the paid checks. Run PAN and GSTIN in batches that stay inside your rate limit, with pacing and capped retries as in the bulk GSTIN guidance. Run the bank account check only for vendors you actually pay, and only once each.
- Treat results as review, not rejection. A legitimate old vendor with a slightly different name should land in a queue, not be blocked mid-contract.
- Record everything with
referenceId. The backfill is the first audit trail for these vendors. - Budget. Count vendors by type, multiply by the calls each type needs, and use the pricing page rates. Standard verifications such as PAN are ₹5 per call and bank account verification is ₹4 per call on pay-as-you-go, with lower plan rates. Sandbox calls are free but return sample data, so they cannot help with a real backfill.
Cost model without surprises
The cost of onboarding one vendor is the number of paid calls multiplied by their rates. The order of checks is what keeps that number low.
| Vendor type | Typical paid calls on a clean path | Notes |
|---|---|---|
| Freelancer | PAN, bank account | Two calls |
| Sole proprietor with GST | PAN, GSTIN, bank account | Three calls |
| Company | PAN, GSTIN, CIN, bank account | Four calls |
| Company that fails at PAN | PAN | One call, because later steps never run |
| Duplicate vendor | None | Caught by the free duplicate check |
Re-checks add calls over the vendor's life. A company vendor re-checked on a quarterly schedule adds a GSTIN call per quarter, and perhaps a CIN call. These are small, but across thousands of vendors they should be in your budget. See the pricing page for the current rates and plan discounts, and remember that every completed call is billable in production, including negative results.
Edge cases to decide in advance
- Foreign vendors. None of these identifiers apply. Route to a separate process with its own checks.
- Government and public bodies. The PAN fourth letter is
GorL, and a CIN may not exist. Do not make a CIN mandatory for every non-individual. - Vendors with several GSTINs. Verify each one on invoices, and confirm they all embed the same PAN.
- Vendors that rename or convert. A conversion from partnership to company, or a rename, can change names and numbers. Ask for the latest certificate when a lookup says not found for a number the vendor insists is theirs.
- Group companies sharing a bank account. Legitimate and common. Route to review, record the relationship and approve with a note.
- Individuals paid through an agency. Decide whether the agency or the individual is your vendor, and verify that one.
Metrics worth watching
| Metric | What it tells you |
|---|---|
| Time from submission to a decision | Whether vendors are being lost to delay |
| Share verified without review | Whether your thresholds suit your vendors |
| Share rejected, by reason | Where bad data and bad actors come from |
| Cases per reviewer per day | Whether the queue is sustainable |
| Duplicates caught | The value of the free check |
| Calls per approved vendor | Whether ordering is keeping costs down |
| Re-check flags raised | How often registrations change under you |
Common mistakes
- Running the bank account check first, or before the PAN has passed.
- Comparing the bank name with the typed name instead of the verified name.
- Skipping duplicate detection, so one entity becomes two vendors.
- Making CIN mandatory for every non-individual vendor.
- Retrying a timed-out bank verification blindly.
- Approving on a GSTIN without comparing its embedded PAN with the verified PAN.
- Treating approval as permission to pay immediately, with no hold and no limits.
- Letting a changed bank account through without re-verification and a callback.
- Rejecting automatically on name differences instead of routing them to review.
- Backfilling the whole vendor base in one run without pacing.
- Using sandbox results to approve real vendors. Sandbox returns sample data.
What to do next
Get a sandbox key through the quickstart and build the flow against the documented test values. Then read the sandbox vs production testing guide before you apply for production, and errors, retries and idempotency before you ship. For what to keep from every call, read what to log and keep. Questions go to support.
API reference at a glance
PAN
POST /v1/verify/panRequest 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, format ABCDE1234F. |
name | string | Name to match against records (max 256 chars). |
POST /v1/verify/pan{
"referenceId": "SMV-20260927-8F42K7",
"pan": "ABCDE1234F",
"type": "Individual",
"valid": true,
"registeredName": "RAHUL KUMAR",
"fatherName": "SURESH KUMAR",
"panStatus": "VALID",
"panStatusCode": "E",
"panStatusDescription": "Existing and valid PAN",
"nameMatchResult": "MATCH",
"nameMatchScore": "100",
"aadhaarSeedingStatus": "y",
"aadhaarSeedingStatusDescription": "Aadhaar linked",
"lastUpdatedAt": "2023-04-01",
"message": "PAN is Valid"
}POST /v1/verify/panPAN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
GSTIN
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, format 07AAAAA1111A1Z1. |
businessName | string | Business (legal or trade) name to cross-check against the registration. |
POST /v1/verify/gstin{
"referenceId": "SMV-20260927-9K21P4",
"verificationId": "VRF-4M8Q2T7XB9KD",
"gstin": "29AAICP2912R1ZR",
"GSTIN": "29AAICP2912R1ZR",
"valid": true,
"legalNameOfBusiness": "UJJIVAN SMALL FINANCE BANK LIMITED",
"tradeNameOfBusiness": "UJJIVAN SMALL FINANCE BANK",
"centerJurisdiction": "I-A RANGE",
"stateJurisdiction": "GUWAHATI - A - 1",
"dateOfRegistration": "2017-09-30",
"constitutionOfBusiness": "Public Limited Company",
"taxpayerType": "Regular",
"gstinStatus": "Active",
"lastUpdateDate": "2022-03-01",
"natureOfBusinessActivities": [
"Retail Business",
"Supplier of Services",
"Recipient of Goods or Services",
"Office / Sale Office",
"Others"
],
"principalPlaceAddress": "First Floor 3512-DISPUR Prithivi Mansion opp. KFC building G.S. Road, Lachit Nagar Assam 781007",
"principalPlaceSplitAddress": {
"buildingName": "Prithivi Mansion",
"street": "G.S. Road",
"location": "Lachit Nagar",
"buildingNumber": "3512-DISPUR",
"district": "Guwahati",
"state": "Assam",
"city": "Dispur",
"flatNumber": "First Floor",
"latitude": "26.1445",
"longitude": "91.7362",
"pincode": "781007"
},
"additionalAddresses": [
{
"address": "Mirza Santipur NH-37 Dist-Kamrup Rural PS-Palashbari, PO Mirza Kamrup Assam 781125",
"splitAddress": {
"buildingName": "",
"street": "NH-37",
"location": "Santipur, Mirza",
"buildingNumber": "",
"district": "Kamrup Rural",
"state": "Assam",
"city": "Palashbari",
"flatNumber": "",
"latitude": "26.0926",
"longitude": "91.5069",
"pincode": "781125"
}
},
{
"address": "N.A.C.C.I. Foundation 3559-Tezpur CHAMBER BHAWAN Binjraj Road, P. o. and P.S. - Tezpur, Dist. Binjraj Road, P. o. and P.S. - Tezpur, Dist. Sonitpur Assam 784154",
"splitAddress": {
"buildingName": "CHAMBER BHAWAN",
"street": "Binjraj Road",
"location": "Tezpur",
"buildingNumber": "3559-Tezpur",
"district": "Sonitpur",
"state": "Assam",
"city": "Tezpur",
"flatNumber": "",
"latitude": "26.6338",
"longitude": "92.8006",
"pincode": "784154"
}
}
],
"message": "GSTIN Exists",
"legalName": "UJJIVAN SMALL FINANCE BANK LIMITED",
"tradeName": "UJJIVAN SMALL FINANCE BANK",
"registrationStatus": "Active",
"principalPlaceOfBusiness": "First Floor 3512-DISPUR Prithivi Mansion opp. KFC building G.S. Road, Lachit Nagar Assam 781007"
}POST /v1/verify/gstinGSTIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
Company CIN
POST /v1/verify/cinRequest 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. |
cinrequired | string | Corporate Identification Number (21 characters) or LLPIN, for example U74999DL2015PTC284009. |
POST /v1/verify/cin{
"referenceId": "SMV-20260927-6R38Y3",
"verificationId": "VRF-6C2N9V4HQ7ZE",
"valid": true,
"status": "VALID",
"cin": "U72900KA2015PTC082988",
"companyName": "COMPANY NAME",
"registrationNumber": 82987,
"incorporationDate": "2015-09-23",
"cinStatus": "ACTIVE",
"email": "[email protected]",
"incorporationCountry": "INDIA",
"directorDetails": [
{
"din": "07168822",
"name": "Frank Kelvin",
"dob": "1997-07-21",
"designation": "DIRECTOR",
"address": "ST BED KORAMANGALA, BANGALORE, KARNATAKA, INDIA, 566634"
},
{
"din": "07168826",
"name": "John Doe",
"dob": "1997-02-07",
"designation": "DIRECTOR",
"address": "GAUTAM NAGAR, NOIDA, UTTAR PRADESH, INDIA, 211361"
}
],
"companyStatus": "ACTIVE",
"directors": [
{
"din": "07168822",
"name": "Frank Kelvin",
"designation": "DIRECTOR"
},
{
"din": "07168826",
"name": "John Doe",
"designation": "DIRECTOR"
}
]
}POST /v1/verify/cinCompany CIN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
Bank Account
POST /v1/verify/bank-accountRequest 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. |
accountNumberrequired | string | 9-18 digit numeric bank account number. |
ifscrequired | string | 11-character IFSC code, e.g. ICIC0001234. |
name | string | Beneficiary name to match. |
phone | string | Account holder's 10-digit Indian mobile number, when available; improves match confidence. |
POST /v1/verify/bank-account{
"referenceId": "SMV-20260927-3B71A0",
"valid": true,
"nameAtBank": "RAVI KUMAR",
"bankName": "ICICI Bank",
"utr": "017952100012345",
"city": "Mumbai",
"branch": "Andheri West",
"micr": "400229002",
"nameMatchScore": "100",
"nameMatchResult": "GOOD_MATCH",
"accountStatus": "ACCOUNT_IS_VALID",
"accountStatusCode": "1",
"ifscDetails": {
"bank": "ICICI Bank",
"ifsc": "ICIC0001234",
"micr": "400229002",
"address": "Andheri West, Mumbai",
"city": "Mumbai",
"state": "Maharashtra",
"branch": "Andheri West"
}
}POST /v1/verify/bank-accountBank Account: ₹4 per successful call, pay-as-you-go (lower on Growth and Scale).
Frequently asked questions
In what order should vendor checks run?
Offline validation, an internal duplicate check, PAN, GSTIN compared with the verified PAN, CIN for companies, bank account with name match, then documents and review. The order follows cost and decisiveness, so a vendor that fails early never triggers a later, dearer call.
Why verify the PAN before the GSTIN and bank account?
The PAN is the anchor. Every GSTIN embeds a PAN, so you can compare it with the verified one, and the verified registered name becomes the reference the bank account name is judged against. Without it you only have what the vendor typed.
Why check for duplicates before calling any API?
A duplicate PAN, GSTIN or bank account in your own data is often the most valuable signal and costs nothing. It also stops one entity becoming two vendors, which is a common way to split approvals or hide spend.
Should bank account verification run last?
Among the automated checks, yes. It involves a bank transfer, so it is the dearest and the one you least want to repeat, and it needs a verified name to compare against. Only vendors who passed the earlier checks should reach it.
How do I avoid double-billing when a job restarts?
Persist every paid check as its own record with kind, input hash, referenceId, outcome and time, and have the orchestrator return the stored result instead of calling again. The referenceId identifies a call but is not an idempotency key, so the dedupe must be yours.
What should I do when a vendor changes their bank account?
Treat it like a new payee: re-verify the account, hold the next payment and call back a known contact through a channel the vendor did not supply in the change request. A changed account is the classic fraud event in vendor payments.
Do all vendors need a GSTIN and a CIN?
No. Ask for what the vendor type can hold: individuals have a PAN, proprietors a PAN and possibly a GSTIN, companies a PAN, GSTIN and CIN. Making CIN mandatory for everyone blocks government bodies, proprietors and many legitimate vendors.
How often should I re-check approved vendors?
By risk: high-spend vendors before payment runs above a threshold and monthly, regular vendors quarterly, occasional ones on next use after a year. Also re-check on events such as a bank account change, a change of signatory or a very large invoice.
How do I verify the vendors I already have?
Segment by risk and spend, run the free offline and duplicate checks across the whole base first, batch PAN and GSTIN calls within your rate limit, check bank accounts only for vendors you pay, treat results as review rather than rejection, and record every referenceId.
Does verification make a vendor trustworthy?
No. It confirms that records exist, are active and agree with each other. It does not detect conflicts of interest, intent to defraud or poor performance, so keep contracts, limits, holds and your own due diligence.
Sources
- StampMitra API reference data for the PAN, GSTIN, CIN and bank account endpoints: fields, errors, scopes and billing, as in this repository on 2026-10-29.
- StampMitra production access form: applicant types, required documents and product risk flags, as implemented in this repository on 2026-10-29.
- GST portal: https://www.gst.gov.in and Ministry of Corporate Affairs: https://www.mca.gov.in (registration and company record concepts; confirm current rules there, 2026).
- Income Tax Department e-filing portal: https://www.incometax.gov.in (PAN structure and holder categories, 2026).