StampMitraDevelopers

Guides

Sandbox vs Production: What You Can Test, and What You Cannot

StampMitra sandbox returns sample data and never verifies a real record, so a sandbox valid says nothing about real PANs, GSTINs or companies. It does prove authentication, scopes, request shape, error handling and webhook handling. Going live is an application with documents, a provisional window, a live key shown once, and a GST-invoiced wallet recharge.

By StampMitra Developer TeamLast updated: Read this in Hindi

The StampMitra sandbox never verifies a real record. It returns sample data: a stable, synthetic answer for every input, with every name in the reply invented. That is exactly what you want from a test environment, and it is also why a sandbox "valid" tells you nothing about whether a real PAN, GSTIN or company exists. What sandbox does prove is everything around the verification: that your key authenticates, that your request is shaped correctly, that you handle every documented error, and that your code reads the response without falling over.

So the useful question is not "does sandbox work like production?" but "which of my risks can sandbox retire, and which only a production call can?" This guide answers that in order. It lists the real sandbox test values, separates what a sandbox pass does and does not prove, lays out a test plan that uses both environments sensibly, and then walks through the actual go-live path on StampMitra: the application, the documents, the provisional limits, the wallet recharge and the GST invoice.

If you want the key-level side of this topic (prefixes, scopes, IP allowlists, rotation), that is a separate guide: sandbox vs production API keys. This one is about what you can learn from each environment and how to cross the gap.

Key takeaways

  • Sandbox returns sample data and never verifies real records. Names in sandbox responses are synthetic. Do not use a sandbox result as evidence about a real person or business.
  • Four magic inputs give predictable outcomes: PAN, GSTIN, CIN and bank account each have one value that returns valid and one that returns not valid. Any other input gets a stable but arbitrary outcome, roughly two valid in three, and the same input always gives the same answer.
  • Sandbox is enough to prove authentication, scopes, request validation, response parsing, error handling for 400, 401, 403 and 429, the shape of referenceId, and that your wallet is not debited.
  • Sandbox cannot prove data accuracy, name-match behaviour on real names, data freshness, real-world status variety, upstream latency or outage behaviour, production rate limits or real cost.
  • Going live is an application, not a switch: a nine-step form, automatic checks, a provisional approval with a 24-hour window for documents, then a live key shown once, followed by a wallet recharge before live calls run.
  • A wallet recharge carries 18% GST. A GST invoice is emailed and saved on the billing page once the payment is confirmed.
  • Plan a small production smoke test with identifiers you own, run it right after go-live, and keep the rest of your test suite in sandbox.

What the sandbox actually is

The environment is decided by the key. A key starting sk_test_ runs in sandbox and a key starting sk_live_ runs in production, with no flag and no different host: both use https://api-b2b.stampmitra.in. In sandbox, calls are metered and logged for you to inspect, but they never debit your wallet.

The responder behind the sandbox is deliberately simple. For a handful of "magic" inputs it returns a fixed result. For every other input it computes a stable but arbitrary outcome from the input, so the same value always gives the same answer, with roughly two in three inputs coming back valid. Nothing is looked up at a registry. Every name in a sandbox reply is synthetic, so the "registered name" you get for a PAN is not the name of anybody.

Two consequences matter for how you test.

First, sandbox outcomes are reproducible, which is a feature. You can assert on them in automated tests without a flaky upstream. Second, they are arbitrary, which is the limit: if your test picks a random-looking PAN and expects it to be invalid, there is a one in three chance it will, and a two in three chance it will not. If a test needs a particular outcome, use the magic values.

e-Stamp in the sandbox

e-Stamp is the one product where the sandbox simulates a whole process and not just a lookup. A sandbox key can create an order and watch it reach a certificate, without any review, payment or issuing service behind it.

Part of the flowWhat the sandbox does
Catalogue and dutyA small sample catalogue and an illustrative fee schedule, with the production field shapes and error wording
OrdersCREATED, PAYMENT_PENDING, PROCESSING, ISSUED by elapsed time, 0, 5, 15 and 30 seconds by default
Failure pathsreference markers such as SBX-PAYMENT-FAILED, SBX-ISSUANCE-FAILED, SBX-CANCELLED, SBX-EXPIRED and SBX-DUPLICATE
WalletsFake Stamp Duty and Service Fee wallets, a top-up that adds credit instantly up to a cap, a 402 when short
CertificateA short-lived link to one shared PDF watermarked SANDBOX SAMPLE, NOT A VALID STAMP
WebhooksThe order.* events, sent to your sandbox endpoints only
Limits50 open orders per organisation, and test orders are deleted after 30 days

Use it to build the order worker, the status mapping, the money handling, the certificate download and the webhook handler. Do not use it to learn how long a real order takes, because production orders wait for a person. The e-Stamp order page lists every status and test input, the payment page describes the fake wallets, and the document page covers the sample certificate.

The documented sandbox test values

These are the values copied from the sandbox responder. Use them in tests that need a deterministic positive or negative.

ProductValue that returns successWhat you getValue that returns failureWhat you get
PANSNDBX0000AValid, status VALID, sample name TEST USERSNDBX9999FNot valid, status INVALID
GSTIN09SNDBX0000A1Z5Valid, Active, sample name TEST BUSINESS PRIVATE LIMITED09SNDBX9999F1Z5Not valid, Cancelled
CINU00000SB0000SANDBOX000001Valid, sample company detailsU00000SB0000SANDBOX999999Not valid
Bank accountSANDBOX0000000001Valid, sample account holderSANDBOX9999999999Not valid

Three notes that save you time.

The PAN and GSTIN values pass format validation: five letters, four digits, one letter for the PAN, and the fifteen-character shape for the GSTIN. The GSTIN values do not carry a valid check character, so if you test your own offline check-character validation, switch it off when the key starts with sk_test_.

The CIN and bank account values do not follow the production formats. The CIN is longer than twenty-one characters and the bank account has letters where the production field expects nine to eighteen digits. The API documents that format problems fail fast with a 400, so if a request with these values is rejected by validation on your account, treat it as a mismatch between the documented magic values and the format check, and ask support rather than working around it in production code. Your own code should not special-case these strings.

Finally, the list is a snapshot. The sandbox responder owns the truth, so if a value stops behaving as listed, ask support instead of assuming the docs are wrong or the sandbox is broken.

Test every outcome by input

Beyond the success and failure pair, each verification product now has its own table of test values that return one documented outcome each: valid, not found, invalid format (the 400), name mismatch where the product takes a name, inactive states such as a cancelled GSTIN, a struck-off company or an expired licence, an upstream timeout or outage (a 502, answered at once), rate limited (429) and wallet balance too low (402). The values are fake but valid in format, such as PAN SNDBX0001F, and each answer carries the status code and error code a live call gives. Add the header X-Sandbox-Delay-Ms (up to 3000) to hold a reply when you want to test your client timeout. Find the table under "Test values" on each product page, for example PAN verification, GSTIN verification and bank account verification. A sandbox call still never debits your wallet.

What a sandbox pass proves

Each of the following can be retired in sandbox and does not need re-proving in production beyond a smoke test.

What you want to knowWhy sandbox answers it
The key authenticates, and the header you chose worksAuthorization: Bearer and x-api-key are both accepted; a wrong key returns 401 UNAUTHORIZED
Scopes are rightCalling an endpoint with a key that lacks its scope returns 403 FORBIDDEN
Your request shape is correctField names, types and endpoint paths are the same as production; malformed values return 400 BAD_REQUEST with a field-naming message
Input validation runs before the callFormat failures are rejected up front, so they cost nothing and appear in sandbox exactly as in production
Your parser handles the responseResponses carry the production field names, including referenceId in the form SMV-YYYYMMDD-XXXXXXX
Your branching on valid and on status fields worksMagic values give both outcomes on demand
Your error handling is complete400, 401, 403 and 429 can all be provoked; the shared error shape { error: { code, message, requestId } } is the same
Your wallet logic does not misfireSandbox never debits the wallet, so a test that expects balance to stay put can assert it
Your logging and masking workYou can log real response shapes without real personal data
Webhook handlers verify and processDelivery attempts can be listed and replayed through the webhook events endpoints, covered below

The rate-limit row deserves one caution. A 429 can be a per-endpoint rate limit (the documented example is 60 requests per 60 seconds for an endpoint) or a sandbox limit: 10 requests a minute or 200 a day per organisation, or 50 a day per IP address across all accounts. The per-minute limit clears within a minute, but the daily and IP limits do not clear until midnight IST. The sandbox kinds do not appear in production. Do not build retry logic that assumes every 429 clears if you wait a few seconds; read the error code (SANDBOX_RATE_LIMIT, SANDBOX_DAILY_LIMIT, SANDBOX_IP_LIMIT) and the Retry-After header.

Webhooks in the sandbox

The webhook events API exists for exactly this kind of testing. GET /v1/webhooks/events (scope webhooks.events.read) lists your organisation's delivery attempts, and POST /v1/webhooks/events/:id/replay (scope webhooks.events.replay) re-fires a specific delivery with the same payload through the same path. You can fire the same event at your handler repeatedly until signature verification, deduplication and processing are all correct. Note what the product documents: webhook events cover the order and payment lifecycle, such as order.created, order.payment_confirmed and order.failed. A verification call is synchronous and returns its answer in the response, so there is no webhook to wait for. See the webhooks page for the event list and signing details.

What a sandbox pass does not prove

This is the section to show your stakeholders before a demo.

What you might assumeReality
"The PAN is real because sandbox said valid"Sandbox says valid for magic values and for about two in three arbitrary inputs. It checked nothing.
"Name matching works on our customers' names"Sandbox names are synthetic. You have not seen the match score on real names, initials, spelling variants or Indic-script names transliterated to Latin.
"Our status handling covers every case"Sandbox shows the statuses it was programmed to show, such as Active and Cancelled for GSTIN. Real registrations include other states, and the reference does not enumerate them.
"Results are fresh"Sandbox has no data source, so it says nothing about how recent the underlying record is. Read why results differ from the government portal.
"Latency will be the same"A sandbox responder answers from memory. Production depends on upstream data sources, and the documented 502 for an unreachable upstream service is something you will mostly meet there.
"Rate limits will be the same"The sandbox plan has its own limits: 10 requests a minute and 200 a day per organisation. Production limits come from your plan and your application approval, including a ramp while documents are pending.
"Cost will be zero"Sandbox never debits the wallet. Production debits it for every completed call, including negative results.
"Everything we can call in sandbox, we can call live"Production products are approved individually, and some are flagged for extra review.

None of these gaps is a defect. They are the reason production smoke tests exist.

A test plan that uses both environments sensibly

Think in three layers, and put each risk in the cheapest layer that can retire it.

Unit tests with recorded fixtures. Save the documented sample responses (the reference data has exact bodies for each endpoint, including error bodies) and run your parsing, mapping and decision code against them. These tests need no network and no quota, and they should cover every branch: valid, invalid, a status you do not recognise, a missing optional field, each error code. Most of your assertions live here.

Integration tests in sandbox. A small set, using the magic values, that proves the real HTTP path: authentication, scopes, headers, timeouts, the success and failure outcome for each product you call, and the errors you can provoke on demand. Keep this set small. The sandbox allows only 200 requests a day per organisation and the acceptable use policy treats sandbox abuse as prohibited, so a test that loops on a 400 burns requests to prove nothing.

Production smoke tests. A handful of calls against identifiers you own and are entitled to check (your own PAN, your company's GSTIN and CIN, a bank account of your own business), run once after go-live and on a schedule if you want canary coverage. These are the only calls that tell you what real data looks like for you. Because they use real personal data, treat the results as such: mask them in logs and do not paste them into tickets.

The first sandbox call is one line of cURL, using the PAN magic value:

cURL
curl -X POST "https://api-b2b.stampmitra.in/v1/verify/pan" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pan":"SNDBX0000A","name":"Test User"}'

Here is the sandbox layer as a Node.js test file using the built-in test runner.

JavaScript
// sandbox.test.mjs  (run: node --test sandbox.test.mjs, with a sandbox key)
import test from "node:test";
import assert from "node:assert/strict";

const key = process.env.STAMPMITRA_API_KEY ?? "";
assert.ok(key.startsWith("sk_test_"), "refusing to run sandbox tests with a non-sandbox key");

const post = (path, body) =>
  fetch(`https://api-b2b.stampmitra.in/v1/verify/${path}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(10_000),
  });

test("PAN magic success returns valid with a referenceId", async () => {
  const res = await post("pan", { pan: "SNDBX0000A", name: "Test User" });
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.equal(body.valid, true);
  assert.match(body.referenceId, /^SMV-\d{8}-[A-Z0-9]+$/);
});

test("PAN magic failure is a completed negative, not an error", async () => {
  const res = await post("pan", { pan: "SNDBX9999F" });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).valid, false);
});

test("a malformed PAN fails fast with 400", async () => {
  const res = await post("pan", { pan: "12345" });
  assert.equal(res.status, 400);
  const body = await res.json();
  assert.equal(body.error.code, "BAD_REQUEST");
  assert.ok(body.error.requestId);
});

test("a wrong key returns 401", async () => {
  const res = await fetch("https://api-b2b.stampmitra.in/v1/verify/pan", {
    method: "POST",
    headers: { Authorization: "Bearer sk_test_not_a_real_key", "Content-Type": "application/json" },
    body: JSON.stringify({ pan: "SNDBX0000A" }),
  });
  assert.equal(res.status, 401);
});

The same checks in Python with pytest:

Python
# test_sandbox.py  (run: pytest -q, with a sandbox key in the environment)
import os, re, pytest, requests

KEY = os.environ.get("STAMPMITRA_API_KEY", "")
BASE = "https://api-b2b.stampmitra.in/v1/verify"

pytestmark = pytest.mark.skipif(not KEY.startswith("sk_test_"), reason="needs a sandbox key")

def post(path, body, key=KEY):
    return requests.post(f"{BASE}/{path}", json=body, timeout=10,
                         headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})

def test_gstin_magic_success():
    r = post("gstin", {"gstin": "09SNDBX0000A1Z5"})
    assert r.status_code == 200
    body = r.json()
    assert body["valid"] is True
    assert re.match(r"^SMV-\d{8}-[A-Z0-9]+$", body["referenceId"])

def test_gstin_magic_failure_is_negative_not_error():
    r = post("gstin", {"gstin": "09SNDBX9999F1Z5"})
    assert r.status_code == 200 and r.json()["valid"] is False

def test_bad_key_is_401():
    assert post("pan", {"pan": "SNDBX0000A"}, key="sk_test_wrong").status_code == 401

Notice the guard at the top of each file. Sandbox tests that run against a live key would spend real money and, worse, call real data sources. Refuse to run unless the prefix is sk_test_.

Guarding the boundary in your application

The same prefix check belongs in your application at startup, in the other direction: production must not start with a sandbox key, and staging must not start with a live one.

JavaScript
// env-guard.mjs
const key = process.env.STAMPMITRA_API_KEY ?? "";
const expected = process.env.APP_ENV === "production" ? "sk_live_" : "sk_test_";
if (!key.startsWith(expected)) {
  console.error(`STAMPMITRA_API_KEY must start with ${expected} when APP_ENV=${process.env.APP_ENV}`);
  process.exit(1);
}

A sandbox key left in a production deployment is the quietest bug in this area. Calls succeed, responses look right, and real customers are verified against sample data. The check costs three lines.

What changes when you go live

Go-live has two halves: getting approved, and getting funded. Both are real steps with real waiting, and neither is a toggle. Start the application early.

The application

The production access application has nine steps: applicant type, signatory, business, your use, compliance, contacts and domains, documents, products and review. The form asks for the identifiers that your applicant type can hold. A sole proprietorship or individual gives a personal PAN. A partnership gives a firm PAN. An LLP gives a firm PAN and its registration number. A company gives its PAN, GSTIN and CIN. The PAN's fourth letter is checked against the applicant type, so a company applicant who enters a personal PAN is told early.

The "your use" and "compliance" steps ask how you will use verification and what you do with the data: who you verify (your customers, vendors, applicants, borrowers), why (customer onboarding, vendor onboarding, a credit decision, employment screening, an agreement or e-stamp workflow, fraud checks), expected monthly volume and peak requests per minute, whether you store results and for how long, whether you are regulated, and whether you have a privacy notice. Some answers carry flags. Higher volume bands ask for more reasoning, and the largest ones point towards a custom plan. The contacts step asks for the people who receive incident notices, invoices and data requests, the state that decides whether GST invoices show CGST and SGST or IGST, and the domains and server IP addresses that will call the API. There is no Aadhaar field anywhere in this flow.

The products step is where you select what you want live. PAN, GSTIN, CIN, Udyam and e-Stamp are treated as lower risk. Bank account verification and e-Sign are flagged as higher risk and open after a short extra check.

Automatic checks, then a provisional key

When you submit, the application is locked and the platform runs checks on what you gave it: the signatory's PAN, the business PAN, the GSTIN, the company or LLP registration, Udyam and the bank account where you supplied them. Each check shows as passed, needing a person to look at it, in progress, or not applicable. The status moves from verifying to in review, and then to a provisional approval where your live key is issued with limited allowances. You can read your answers afterwards, but corrections go through support so a named person records the change.

The 24-hour document window

A provisional approval starts a countdown: your documents are due within a window that the bundled configuration sets at 24 hours, with reminders at 12, 4 and 1 hours before it ends. The documents depend on applicant type.

Applicant typeDocuments typically required
IndividualAddress proof
Sole proprietorshipUdyam certificate, current account proof (cancelled cheque or statement), address proof
Partnership firmPartnership deed, authorisation letter
LLPCertificate of incorporation, GST registration certificate, authorisation letter, board resolution unless the signatory is a director
Private or public companySame four as LLP
Trust or societyTrust deed or society registration, authorisation letter

Files are limited to JPEG, PNG or PDF of up to 5 MB each. Our onboarding team can upload on your behalf and can extend the window when you ask, recording the reason and the person; the bundled configuration allows up to two extensions. If the time passes with documents missing, the application closes by itself and you can apply again with your answers pre-filled, typing your PANs again. Support can reopen a closed application for a limited period.

While documents are pending, the provisional limits apply. The bundled defaults are 60 requests per minute and 5,000 calls per day, and the status page shows the numbers for your application. The limits lift when the documents are approved.

The live key and the wallet

Once the live key is issued, it is shown once. The owner also receives an emailed one-time link to see it, and if you rotate the key later, the new one is shown on the API keys page. Before live calls can run, the wallet needs money. The recharge has a minimum amount (the recharge box shows it) plus 18% GST. StampMitra invoices from Delhi, state code 07, so a customer billed in another state pays IGST, while a Delhi customer sees CGST and SGST. A GST invoice is emailed to you and saved on the billing page as soon as the payment is confirmed.

The wallet then drives four states you should know in advance. With enough balance, live calls run. When the balance is too small for the calls you plan, you are asked to recharge before starting. When it runs low during use, a banner says so. When the balance reaches zero, live calls pause until you recharge, and sandbox keeps working. The call that fails for lack of funds returns 402 PAYMENT_REQUIRED with the message Insufficient fulfilment wallet balance to cover this call., which your error handling must treat as an operations problem, not a retry candidate.

Differences you will see on day one

AspectSandboxProduction
Key prefixsk_test_sk_live_
DataSynthetic, stable, arbitraryReal records from data sources
Names in responsesInventedReal names on record, which are personal data
WalletNever debitedDebited per completed call, negative results included
Failures you see400, 401, 403, quota 429Those plus 402 for an empty wallet, per-endpoint 429 and 502 for an unreachable upstream
LimitsSandbox limits: 10 a minute, 200 a day per organisationPlan limits, and provisional limits until documents are approved
LogsVisible to you, for testingVisible to you, retention depending on your plan
BillingNonePer-call charges and GST invoices for wallet recharges

A first-day production checklist

  1. Confirm the deployed key starts with sk_live_ and your guard passes.
  2. Check the wallet balance and the recharge state before the first call, and confirm the billing email receives the GST invoice.
  3. Make one call per product with identifiers you own. Compare the response with what you expect. Record the referenceId.
  4. Read the request logs for those calls through the dashboard or the logs endpoint, and confirm they match what you sent.
  5. Provoke one error on purpose, for example a malformed PAN, and confirm your alerting sees it.
  6. Check that your logs mask PANs and account numbers.
  7. Watch the status of the application until documents are approved and the provisional limits lift.
  8. Set a low-balance alert in your own monitoring, not only the banner in the dashboard.

How to provoke each error on purpose

Error handling that has never run is a guess. Every documented failure can be triggered deliberately in sandbox, and doing so before launch is the cheapest insurance you have. The trick is to know which input produces which status.

To provokeDo thisExpect
400 BAD_REQUESTSend a PAN that is not five letters, four digits and one letter, or omit a required fieldA message naming the field and the expected format, for example pan must match the format ABCDE1234F, plus a requestId
401 UNAUTHORIZEDSend a made-up key, or omit the headerMissing or invalid API key.
403 FORBIDDENCreate a key with only the PAN scope, then call the GSTIN endpointAPI key is valid but lacks the required scope for this endpoint.
429 TOO_MANY_REQUESTSFire more than the endpoint limit inside a window, from a throwaway scriptRate limit exceeded: 60 requests per 60 seconds for this endpoint.
Negative but successfulUse the failure magic valueHTTP 200 with valid false, referenceId present

Two cases are not in the table because you should not provoke them casually. A 402 needs an empty wallet, which sandbox never creates because it never debits, so test your 402 branch with a recorded fixture and confirm it against production when the wallet first runs low. A 502 needs an unreachable upstream service, which you cannot trigger on demand; test that branch by pointing a client at a local server that returns a 502 with the documented body.

A few habits make these tests honest. Assert on the HTTP status and the error.code, and treat the message text as a hint rather than a contract, because wording can be refined. Always assert that requestId is present, since it is what you will give support. And test the negative-but-successful case separately from the error cases: a valid: false response arrives with a 200 and is a completed, billable lookup in production, so it must not be handled by your error path.

Test data hygiene

Sandbox makes it tempting to type real identifiers into forms "just to see". Resist it, for three reasons. A sandbox answer about a real PAN is meaningless, so you learn nothing. A screenshot or a log line with a real number and a synthetic "valid" can later be mistaken for a verification record. And your own colleagues' PANs and bank accounts are personal data you have no purpose to hold.

Keep a small, versioned file of test identifiers: the magic values above plus a few well-formed inventions for validation tests. Use the same documented placeholders in screenshots, ticket descriptions and shared Postman collections. In production smoke tests, use identifiers you are entitled to check, store them as secrets rather than in the repository, and mask them in logs.

Reading the logs to confirm what your tests did

Every call is logged against your organisation, and the logs are queryable with an API key. GET /v1/logs returns paginated items with an id, a createdAt time, the actorType, the service code such as pan.verify, and metadata, filterable by from, to, service and status (success or failure), with a limit of up to 200 and a nextCursor for the next page. GET /v1/usage/summary gives the totals for the current billing period, calls by service and calls by day.

Use them as an assertion after a test run. If your integration suite should have made fourteen calls to pan.verify and the usage summary shows forty, something is retrying, looping or running in parallel where you did not expect it. In production, the same two endpoints let you reconcile your own counters against the platform's before you reconcile against an invoice. How long logs are kept depends on your plan, from a short window on the sandbox plan to a year on the highest self-serve plan, so export anything your compliance policy needs rather than assuming the dashboard is your archive. The guide on what to log and keep covers that decision.

A demo script that stays honest

Demos are where sandbox gets mistaken for production. If you show StampMitra-backed verification to a customer, an investor or your own leadership, say what the screen is doing.

Open by saying the environment: "This is a sandbox key, so responses are sample data." Use the magic values so the outcome is predictable, and narrate both: a valid PAN returns a synthetic name, an invalid one returns a completed negative. Show the error handling on purpose by sending a malformed value and a wrong key. Do not type a real person's PAN. If the audience asks "is that a real company?", the correct answer for any sandbox response is no.

Close by describing what the live version will add: real records, real names that you must handle as personal data, the wallet debits and the provisional limits during approval. A demo that is clear about this earns trust; a demo that lets the audience believe sandbox data is real creates a problem you will have to correct later.

A worked timeline: a two-week path from first call to live traffic

Consider a small team building vendor onboarding for a trading business. They need PAN, GSTIN and bank account checks. Here is how they might spread the work across both environments.

Day 1 to 2. One developer signs up, gets a sandbox key and makes the first call with SNDBX0000A. They record the documented sample responses as fixtures and write the parser against those. The PAN, GSTIN and bank account clients are separate small modules, each with unit tests that cover valid, invalid, an unrecognised status and every documented error body.

Day 3 to 5. They build the onboarding flow itself with the three clients stubbed. The decision logic (which results pass, which go to review) is tested only against fixtures. No network calls are made. The sandbox limits are untouched.

Day 6. They write the small sandbox integration suite: one positive and one negative for each product using magic values, plus a malformed value, a wrong key and a wrong scope. About a dozen calls in total. The suite refuses to run without a sk_test_ key.

Day 7. The company's finance lead starts the production application, because it needs the company's PAN, GSTIN and CIN, a signatory, domains and server IPs, and a choice of products. The team selects PAN and GSTIN as lower risk and bank account as the product that will take an extra check. They describe the use case specifically: vendors, not consumers, with the volume band and the retention period they have agreed internally.

Day 8 to 9. Automatic checks finish and the application moves to a provisional approval. The 24-hour clock starts for documents. The finance lead uploads the incorporation certificate, GST certificate, authorisation letter and, because the signatory is a director, no board resolution is needed. The live key is issued and shown once. It goes into the secret manager and nowhere else.

Day 10. The wallet is recharged. The GST invoice arrives by email and appears on the billing page, and finance files it. The team deploys with the live key to staging first, which fails at startup because of the environment guard. That is the guard working: staging is meant to use sk_test_. They fix the secret mapping and redeploy.

Day 11. In production, they run the smoke test with the company's own PAN and GSTIN. The returned names and statuses are what finance expected. One response includes a status the fixtures did not cover, and the team adds it to the review path. This is exactly the kind of surprise sandbox cannot produce.

Day 12 to 14. Documents are approved, the provisional limits lift, and the bank account product opens after its extra review. They turn on a low-balance alert in their own monitoring, and reconcile one day of the usage summary against their own counters. Real vendors start to flow through.

Nothing in that timeline needed more than a dozen sandbox calls, and the only live calls before real traffic were the smoke test. That ratio is the point.

Cost and quota thinking for tests

Sandbox is free of wallet debits, but it is not unlimited. Treat it as a budget: record fixtures, keep integration tests few, and do not test inside a loop. In production, a completed negative result is a billable call, so a test that checks an invalid PAN pays for it. A standard verification such as PAN is ₹5 per call and bank account is ₹4 per call on pay-as-you-go. See the pricing page for the current plans and rates.

Common mistakes

  • Reading sandbox "valid" as evidence about a real record. Nothing was looked up.
  • Using arbitrary values in a test that needs a specific outcome, instead of the magic values.
  • Running an offline GSTIN check-character test in sandbox and having it reject the documented values.
  • Leaving a sandbox key in a production deployment, or a live key in a developer's local file.
  • Building retry logic around 429 as if it always clears, when a sandbox daily-limit 429 does not clear until midnight IST.
  • Applying for production late, then discovering the document window, the review of higher-risk products and the wallet recharge all take time.
  • Skipping the production smoke test, so the first real name-match score is seen by a customer.
  • Pasting real production responses into tickets or chat.

What to do next

If you do not have a sandbox key yet, the quickstart takes about five minutes. Get your handling of failures right with errors, retries and idempotency. Decide what to keep from each call with what to log and keep. When you are ready, apply for production from your dashboard, and read the pricing and wallet details on the pricing page. For the sample values used in this article, see the endpoint pages for PAN and GSTIN.

API reference at a glance

PAN

PAN: paramsPOST /v1/verify/pan

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.
panrequiredstringPAN, format ABCDE1234F.
namestringName to match against records (max 256 chars).
PAN: responsePOST /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"
}
PAN: pricePOST /v1/verify/pan

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

GSTIN

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, format 07AAAAA1111A1Z1.
businessNamestringBusiness (legal or trade) name to cross-check against the registration.
GSTIN: responsePOST /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"
}
GSTIN: pricePOST /v1/verify/gstin

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

Frequently asked questions

Does the StampMitra sandbox verify real PANs or GSTINs?

No. Sandbox returns sample data from a simple responder and never looks anything up at a registry. Every name in a sandbox response is synthetic, so a sandbox valid result is not evidence about any real person or business.

What are the sandbox test values?

PAN SNDBX0000A returns valid and SNDBX9999F returns not valid. GSTIN 09SNDBX0000A1Z5 returns valid and active and 09SNDBX9999F1Z5 returns not valid and cancelled. CIN and bank account each have a success and a failure value too. Any other input gets a stable but arbitrary outcome.

What does a sandbox pass prove?

It proves authentication, scopes, request shape, input validation, response parsing, handling of 400, 401, 403 and 429 errors, that your wallet is not debited, and that your webhook handler verifies and processes deliveries. It does not prove data accuracy, name-match behaviour on real names or data freshness.

Why did my own GSTIN check character test reject the sandbox values?

The documented sandbox GSTINs and the sample GSTIN in the docs are placeholders without a valid check character. Run your offline check-character validation only when the key starts with sk_live_ and skip it for sk_test_ keys.

Can I test webhooks in the sandbox?

Yes. List your delivery attempts with GET /v1/webhooks/events and re-fire one with POST /v1/webhooks/events/:id/replay, using the webhooks.events.read and webhooks.events.replay scopes. Webhook events cover the order and payment lifecycle, while verification calls return their answer synchronously.

How do I get production access?

Apply from the dashboard through a nine-step form covering applicant type, signatory, business, use, compliance, contacts, documents, products and review. After automatic checks you get a provisional approval and a window, set at 24 hours in the default configuration, to upload documents.

What happens if I miss the 24-hour document window?

The application closes by itself and you can apply again with your answers pre-filled, typing your PANs again. The onboarding team can also extend the window when you ask, up to the number of extensions the configuration allows, and support can reopen a closed application for a limited period.

When is my wallet charged and what is the GST invoice?

Sandbox never debits your wallet. In production each completed call is debited, negative results included. Recharges carry 18% GST, and a GST invoice is emailed and saved on your billing page once the payment is confirmed.

What happens when the wallet balance reaches zero?

Live calls pause until you recharge and the API returns 402 PAYMENT_REQUIRED with the message Insufficient fulfilment wallet balance to cover this call. Sandbox keeps working. Treat a 402 as an operations alert, not as something to retry.

Should I run production smoke tests, and with what data?

Yes, a handful of calls right after go-live, using identifiers you own and are entitled to check, such as your company's own PAN, GSTIN and CIN. They are the only calls that show what real data looks like for you, so mask the results in logs.

Sources

  • StampMitra sandbox test values, copied from the sandbox responder, and the sandbox behaviour described here: this repository, reviewed 2026-10-29.
  • StampMitra production access flow: application steps, applicant types, required documents, document window, provisional limits and wallet prompt states, as implemented in this repository on 2026-10-29. Window, limits and reminder values are the bundled defaults and may be overridden by the server.
  • StampMitra API reference data for error shapes, referenceId, scopes and webhook events endpoints, 2026-10-29.
  • StampMitra developer terms and acceptable use policy on sandbox use, current at 2026-10-29.