The StampMitra API reports every failure in one JSON shape, { "error": { "code", "message", "requestId" } }, and the HTTP status tells you what to do next. Retry a 429 (rate limited) after a short, growing pause, and retry a 502 (upstream unreachable) a couple of times. Do not retry 400 (fix the input), 401 or 403 (fix the key or its scopes), or 402 (top up the fulfilment wallet first). A 404 means the resource does not exist for your organisation.
This guide covers each documented error with its cause, the client action, retry rules, user-facing wording, logging and alerting. It ends with a decision table, reusable Node.js and Python error-handling modules, and an incident routine that uses the status page and support. Every status code, code string and sample body here comes from the shared error definitions in the StampMitra API reference data, and anything the reference does not document is flagged instead of guessed.
If you are integrating a lookup for the first time, read verify a PAN in Node.js and Python alongside this page. If you are wiring up money-moving calls, pair it with the e-Stamp order flow guide.
Key takeaways
- Every error from the StampMitra API has the same body:
error.code,error.messageanderror.requestId. Branch on the HTTP status first and thecodesecond. Treatmessageas display and logging text, not as a contract. - The documented statuses are 400, 401, 402, 403, 404, 409, 429 and 502. Only 429 and 502 are candidates for automatic retry.
- 402 means the fulfilment wallet cannot cover the call. It is an operations problem (top up), never a code problem, so alert a human and pause the affected feature instead of looping.
- 400 on a verification route means the input failed format validation before anything was submitted. Validate on your side first and you will rarely see it.
- 401 and 403 are different: 401 is a missing or invalid key, 403 is a valid key without the scope the endpoint needs.
- Always log the
requestId. It is the one value support needs to trace a call, and it is returned on every error body. - The reference data does not document a
Retry-Afterheader, a rate-limit header set, or idempotency on verification calls. Build your backoff on your own timers and do not assume headers that are not documented. - Show users a short, calm message per error class. Keep raw
messagetext, keys and personal data out of user screens and out of logs.
What does a StampMitra API error look like?
Every error the API returns is normalised by one exception filter into a single envelope. The reference data describes it as the one shape every endpoint can return, and it states that stack traces and raw database errors are never leaked. That consistency is the most useful property for client code: you write one parser, and every endpoint (verification, e-Stamp, orders, payments, team) is covered.
A typical failure looks like this:
{
"error": {
"code": "PAYMENT_REQUIRED",
"message": "Insufficient fulfilment wallet balance to cover this call.",
"requestId": "a1b2c3d4-e5f6-4a1b-8c2d-9f0e1a2b3c4d"
}
}The three fields and how to use them
code is an upper-case string such as BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, PAYMENT_REQUIRED, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS or INTERNAL_ERROR. It maps one-to-one onto the status in the reference examples, so you can use it for metrics labels and for switch statements that read better than bare numbers.
message is a human-readable sentence. On a 400 it names the failing field and the expected format, for example pan must match the format ABCDE1234F. That is helpful to a developer reading a log, but the exact wording can change, so do not parse it with regular expressions to decide control flow. Use it only for display in developer tooling and for log search.
requestId is a UUID that identifies this request on StampMitra's side. Capture it every time, on every error. When you open a support ticket, it is the first thing the team will ask for. One caveat from the samples: the 502 example carries a different requestId from the others, which is simply because each response gets its own.
What is not in the envelope
The envelope has no details array, no per-field error list and no documented retryable flag. If a request has two bad fields, the sample bodies show one message, so plan to fix and resubmit rather than expecting a list. Likewise the success bodies are different shapes per product (a PAN response carries referenceId, valid, panStatus and so on), but you can always tell a failure by the HTTP status and the presence of the top-level error key.
Quick parse recipe
- Read the HTTP status. If it is 2xx, parse the product-specific success body.
- Otherwise parse JSON defensively; a gateway or network layer in front of any API can return a non-JSON body, so wrap the parse in a try/catch and fall back to a generic error.
- Pull
error.code,error.messageanderror.requestIdif present. - Decide retry or not from the status (the decision table below), log, and surface a user message.
Which status codes can the API return, and what does each mean?
The shared definitions in the StampMitra reference data cover the set below. Verification routes (/v1/verify/*) combine the auth errors, 400, 402, 429 and 502. The e-Stamp and order routes use the same envelope. The password-auth and team routes add 409 and a few 400 variants.
| Status | code | Documented example message | Meaning | Retry? |
|---|---|---|---|---|
| 400 | BAD_REQUEST | pan must match the format ABCDE1234F | Validation error, or the call is not allowed for the account | No, fix the request |
| 401 | UNAUTHORIZED | Missing or invalid API key. | Key missing, malformed, revoked or wrong | No, fix credentials |
| 402 | PAYMENT_REQUIRED | Insufficient fulfilment wallet balance to cover this call. | Wallet cannot cover a priced call | No, until topped up |
| 403 | FORBIDDEN | API key is valid but lacks the required scope for this endpoint. | Valid key, missing scope | No, change the key's scopes |
| 404 | NOT_FOUND | The requested resource was not found. | Resource id does not exist for you | No |
| 409 | CONFLICT | A record with these details already exists. | Duplicate on signup-style routes | No |
| 429 | TOO_MANY_REQUESTS | Rate limit exceeded: 60 requests per 60 seconds for this endpoint. | Rate limit hit | Yes, with backoff |
| 502 | INTERNAL_ERROR | Unable to reach the order/customer service right now | Upstream verification service unreachable | Yes, bounded |
404 and 409 in more detail
The 404 example appears on team routes (revoking an invite, changing a member's role, removing a member) with the message The requested resource was not found.. Treat it as a stale id or wrong organisation, not as a transient problem. A 404 on GET /v1/orders/:id has the same meaning: the id is not one your organisation owns. Retrying will not help, so correct the id or show a not-found state.
The 409 example appears on dashboard signup when the email is already registered. It is rare on the server-to-server API surface you call with a key, but a robust client maps it to a non-retryable "already exists" outcome and moves on.
Statuses you may meet that the reference does not list
A proxy, load balancer or your own network can produce 5xx statuses, 504 timeouts or connection resets that never reach StampMitra's exception filter. The reference examples only include 502 as a 5xx. Treat any other 5xx like 502 for retry purposes, bounded in the same way, and make sure your parser copes with a non-JSON body. Do not invent code strings for them; log the raw status and a truncated body instead.
How do you decide between retry, fail and alert?
The mistake that costs the most in production is not a wrong error message. It is a retry loop around an error that cannot succeed, which burns rate limit, floods logs and delays the real fix. Decide per status, once, in one place. The table below is the rule set the reusable modules later in this guide implement.
| Status | Auto retry | Max attempts | Wait before retry | Human action | Alert level |
|---|---|---|---|---|---|
| 400 | Never | 0 | n/a | Fix input or fix account entitlement | Low (log only), page if sudden spike |
| 401 | Never | 0 | n/a | Rotate or correct the key | High, a deploy or secret problem |
| 402 | Never | 0 | n/a | Top up the fulfilment wallet | High, business-impacting |
| 403 | Never | 0 | n/a | Grant the missing scope, or use the right key | High on first sight, then deduplicate |
| 404 | Never | 0 | n/a | Fix the id | Low |
| 409 | Never | 0 | n/a | Treat as already done | Low |
| 429 | Yes | 3 to 5 | Exponential with jitter, start near 1 second | Reduce concurrency or request a higher limit | Medium if sustained |
| 502 | Yes | 2 to 3 | Short exponential with jitter | Check status page if it persists | Medium, high if sustained |
Why only two statuses retry
Retrying is only sensible when the same request could plausibly succeed a moment later. A 400 is deterministic: the same body fails the same way. A 401 or 403 depends on the key and its scopes, which do not fix themselves between attempts. A 402 depends on wallet balance, which changes only when a human or a payment flow tops it up. A 404 and a 409 describe the state of a record. A 429 clears as the window passes, and a 502 clears when the upstream service returns.
The retry budget idea
Give each logical operation a budget, for example three attempts and a total deadline of ten seconds for an interactive request, or longer for a background job. When the budget is spent, stop and return a typed failure. Without a deadline, a burst of 429s multiplies into a retry storm that makes the rate limit worse for everyone sharing your key.
What the data does not promise
The reference data documents no Retry-After header and no rate-limit headers, so the wait times above are your own policy, not a server instruction. It also documents no idempotency on verification calls: the note on those endpoints says referenceId is StampMitra's own transaction id for the call and "is not an idempotency key". The only idempotency-related field in the data is an optional Idempotency-Key request header on POST /v1/orders and POST /v1/payments/intents, described just as "Optional idempotency key". How duplicates are resolved is not specified, so test it in sandbox and confirm with support before you depend on it.
What should you do about a 400 Bad Request?
A 400 on a verification route means StampMitra rejected the input before submitting anything for verification. The reference notes say input format is validated up front, so a malformed identifier fails fast and is never sent for verification. Each product has its own message, and all of them follow the pattern " must match the format ".
Validation messages from the reference
| Product | Field | Documented 400 message |
|---|---|---|
| PAN | pan | pan must match the format ABCDE1234F |
| GSTIN | gstin | gstin must match the format 07AAAAA1111A1Z1 |
| Vehicle RC | vehicleNumber | vehicleNumber must match the format MH12AB1234 |
| Voter ID | voterId | voterId must match the format ABC1234567 |
| Driving licence | dlNumber | dlNumber must match the format MH1420110012345 |
| Passport | fileNumber | fileNumber must match the format PA1079341954215 |
| Company CIN | cin | cin must match the format U74999DL2015PTC284009 |
| Udyam | udyamNumber | udyamNumber must match the format UDYAM-MH-12-1234567 |
| Bank account | accountNumber | accountNumber must be a 9-18 digit numeric bank account number |
The two derived lookups, PAN to GSTIN and PAN to Udyam, reuse the PAN message. The product pages such as PAN verification and GSTIN verification list the exact parameters.
What the client should do
Do not retry. Instead, validate in the browser or in your service before you call, with the same shapes as the sample messages, so users get instant feedback and you spend no call on an obviously wrong value. For example, a PAN field can enforce five letters, four digits and one letter before it ever leaves your form. When a 400 still arrives, show the user a correction prompt and keep the failed value out of your logs.
A 400 can also mean "not entitled"
The reference labels the metering-level 400 as "Validation error / not entitled". That means a 400 can also appear when your organisation is not entitled to a product in production. The data does not give a separate code or message for it, so you cannot tell the two apart from the status alone. If a value that matches the format keeps returning 400 in production while it works in sandbox, check your product entitlements in the dashboard and ask support with the requestId. The sandbox versus production keys guide explains why entitlement differs by environment.
Other 400 causes on non-verification routes
Dashboard routes return a 400 for a weak password (the message lists the strength rule), a wrong or expired OTP (This code is invalid or has expired.), an invalid or expired email-verification link, and attempts to demote or remove an organisation's last owner. These are user-correctable too, and the same no-retry rule holds.
Worked example
curl -X POST "https://api-b2b.stampmitra.in/v1/verify/pan" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pan":"ABCDE123F","name":"Rahul Kumar"}'The PAN above has nine characters, so the expected response is a 400 with the documented envelope:
{
"error": {
"code": "BAD_REQUEST",
"message": "pan must match the format ABCDE1234F",
"requestId": "a1b2c3d4-e5f6-4a1b-8c2d-9f0e1a2b3c4d"
}
}What is the difference between 401 and 403, and how do you fix each?
Both are credential problems, but they have different causes and different fixes, and mixing them up wastes debugging hours.
401 Unauthorized: the key itself is the problem
The documented body is UNAUTHORIZED with Missing or invalid API key.. It covers a missing header, a typo, a truncated key, a key that was revoked or rotated, and a key sent in the wrong header. The API accepts Authorization: Bearer <key> or x-api-key: <key>, and nothing else. The authentication docs show both forms.
The same message appears when you call the session-style endpoint GET /v1/auth/password/me without a session, which is a reminder that dashboard cookie routes and API-key routes are separate. Server integrations use keys; the cookie flow is for the dashboard.
Client action: do not retry, because the same key will fail again. Check which environment's key the process actually loaded (a sk_test_ key versus an sk_live_ key), whether the secret is present in this deployment, and whether someone rotated it. Alert quickly: a 401 in production nearly always means a broken deploy or a rotated secret, and it will affect every request until fixed.
403 Forbidden: valid key, wrong scope
The documented body is FORBIDDEN with API key is valid but lacks the required scope for this endpoint.. Keys carry OAuth-style scopes, and each endpoint names its required scope in the reference. A few examples from the data:
| Endpoint | Required scope |
|---|---|
POST /v1/verify/pan | verification.pan.read |
POST /v1/verify/gstin | verification.gstin.read |
POST /v1/verify/bank-account | verification.bank-account.read |
GET /v1/estamp/states | estamp.read |
POST /v1/estamp/duty/calculate | estamp.duty.calculate |
POST /v1/orders | orders.create |
POST /v1/payments/intents | payments.write |
GET /v1/orders/:id/documents | documents.read |
Client action: do not retry. Compare the scope the endpoint needs with the scopes on the key you used, then issue or update a key with the right scope, following least privilege. Sensitive scopes can also be restricted to an IP allowlist according to the authentication reference. The data does not say which status a blocked IP receives, so if a key with the correct scope is rejected only from some servers, check the allowlist and ask support for the requestId trace.
On team routes, the same 403 body is shown for a caller who is not an owner or admin, and on login the data shows Please verify your email before logging in. for an unverified account. These are dashboard concerns, but the handling is the same: surface a clear message, no retry.
Security note
Never echo the key in an error log, and never include it in a user-visible message. Log the key's prefix (for example the first characters of sk_test_) and your own key label if you need to identify which key failed. The API security policy describes StampMitra's expectations around credentials.
What does a 402 mean and how do you recover from it?
A 402 is the error that most often surprises teams, because it is not about your code at all. The documented body is PAYMENT_REQUIRED with Insufficient fulfilment wallet balance to cover this call.. In plain terms, you called a priced service and the fulfilment wallet attached to your organisation could not cover the charge.
How metering and the wallet interact
Per the reference, verification calls are metered after a successful response: "usage is only recorded (and the fulfilment wallet debited, for priced services) once verification completes successfully." Both a positive and a negative determination count as completed, billable verifications. The data also says calls on e-Stamp duty calculation and parties attach are metered against the fulfilment wallet. Sandbox calls are metered and logged but do not debit your wallet, so a 402 is a production-side signal. For current per-call rates see pricing; for a PAN lookup the page shows ₹5 per call.
What the client should do
- Stop the call. A retry will fail identically until the balance changes.
- Mark the feature degraded in your system so the same request is not hammered by every user.
- Alert the people who own the wallet (finance or the engineering owner), not only the on-call developer.
- After the wallet is topped up, resume. A simple approach is a circuit breaker that opens on 402 and lets one probe call through every few minutes.
What to tell the user
Never show "insufficient wallet balance" to your end customer. It exposes your billing arrangement and offers nothing the customer can do. Use a neutral message such as "We cannot complete this check right now. Please try again later," and queue the work if your flow allows it. For a KYC step, consider saving the application as pending so no one has to restart.
Preventing a 402 in the first place
Track your own call count against the balance, set a low-balance threshold, and alert on it before it reaches zero. The API gives you no documented balance endpoint in the reference data, so monitor through the dashboard and your own metrics of successful calls multiplied by the product rate. Teams with spiky workloads, such as month-end bulk onboarding, should top up ahead of the batch rather than discover the shortfall mid-run.
Worked example
A lending startup (invented here) runs a nightly job that verifies 5,000 PANs. At 02:10 the job gets a 402 on the 3,812th call. A naive worker keeps going and logs 1,188 identical errors. A good worker opens its circuit on the first 402, writes the remaining records as pending_funds, raises one alert with the requestId, and exits cleanly. After the top-up, the job resumes from the saved checkpoint with no duplicated work.
How do you handle 429 rate limits without making things worse?
A 429 means you sent more than the allowed volume. The documented body is TOO_MANY_REQUESTS with Rate limit exceeded: 60 requests per 60 seconds for this endpoint.. The authentication reference states that requests are subject to per-endpoint rate limits enforced server-side, and the product overview notes that write endpoints are rate-limited per API key. Treat the 60-per-60-seconds figure as the value shown in the example, not as a promise for every endpoint, and check your own observed limit before sizing concurrency.
Two different 429 situations
The 429 status appears in two situations according to the platform copy. One is the ordinary per-endpoint rate limit. The other is the lifetime sandbox call quota: the home page says going over the quota returns "a clear 429, never a silent failure," and the support FAQ says a 429 can mean "you hit a rate limit or your lifetime sandbox quota." The reference does not publish a separate message or code for the quota case.
That distinction matters because the cures differ. A rate limit clears by waiting. A lifetime quota does not clear by waiting, so retrying forever is pointless. This is why the module later in this guide stops after a bounded number of attempts and reports the final failure instead of retrying indefinitely.
Backoff recipe
Use exponential backoff with jitter: wait about one second, then two, then four, adding a random fraction so a fleet of workers does not retry in lockstep. Cap the wait (for example at 30 seconds) and the attempts (three to five). Because no Retry-After header is documented, do not read one; if the response happens to include it, honour it as a bonus but do not depend on it.
Reduce the cause, not only the symptom
- Cap concurrency per endpoint with a semaphore sized below the limit you observe.
- Smooth bulk jobs with a token bucket instead of firing in bursts.
- Cache results where it is lawful and sensible for your use case. A PAN status that you verified a minute ago in the same onboarding session does not need a second call. Keep retention aligned with your own policy and the developer privacy policy.
- De-duplicate: if a user double-clicks "Verify", send one request.
What to tell the user
For an interactive call, if the retry budget runs out, say "We are busy right now. Please try again in a minute." For a background job, re-queue with a delay. Alert when 429s stay above a small percentage of calls for several minutes, since a sustained rate means a capacity or design problem rather than a blip.
What should you do when you get a 502?
A 502 has the code INTERNAL_ERROR and the message Unable to reach the order/customer service right now. According to the reference comments, it is returned whenever the upstream verification service is unreachable or returns an unexpected error, and it is shared across every Core-backed /v1/verify/* route. Notably, the status is 502 while the code string is INTERNAL_ERROR, so do not branch on the code string alone when classifying this one; use the status.
Is it safe to retry?
For verification calls, the metering note is reassuring: usage is recorded and the wallet debited only once verification completes successfully. A 502 is a failure, so on the face of the data you should not have been billed for the failed attempt. The reference does not state this as an explicit guarantee about every failure path, so reconcile your own successful-call counts with usage in the dashboard when you first go live.
The risk is the ambiguous case: a connection that times out on your side, where you never saw a response. The call might have completed on the server. For a read-style lookup, a retry is usually harmless apart from a possible second billable call. For order creation and payment intents, a blind retry could create a duplicate, which is exactly why those two endpoints accept the optional Idempotency-Key header. Use it, and test its behaviour in sandbox.
Retry policy
Retry up to two or three times with a short exponential delay (for example 500 ms, then 1 s, then 2 s with jitter) and a hard deadline. If it still fails, stop and degrade gracefully. Do not hold a user on a spinner for a minute.
Degrade, do not blank out
Decide ahead of time what your product does when verification is unavailable. Options include letting the user continue with the step marked "verification pending", queueing the check for a background retry, or routing to manual review. Which is acceptable depends on your regulatory and risk position, so confirm it with your compliance team. StampMitra provides the lookup, and the decision about what to allow without it is yours.
When 502s mean an incident
One isolated 502 is noise. A run of them across many requests is a signal. Check the status page, compare against your 502 rate baseline, and follow the incident routine later in this guide.
How should you word error messages for end users?
Users do not need to see PAYMENT_REQUIRED or a UUID. They need to know whether to fix something, wait, or contact someone. Keep a small mapping from your internal error class to a message, and keep the raw API text out of the interface. This also helps compliance, because raw upstream messages can contain submitted identifiers.
| Error class | Suggested user message | Show request reference? |
|---|---|---|
| 400 on a field | "Please check the PAN and try again. It should look like five letters, four digits, one letter." | No |
| 401, 403 | "We could not complete this check because of a configuration issue on our side. Our team has been notified." | Yes, for support |
| 402 | "We cannot complete this check right now. Please try again later." | Yes, for support |
| 404 | "We could not find that record." | No |
| 429 after retries | "We are very busy. Please try again in a minute." | No |
| 502 after retries | "The verification service is temporarily unavailable. Your details are saved, and we will retry shortly." | Yes, for support |
Principles
Be specific where the user can act, vague where they cannot. Do not blame the user for a server-side problem, and do not reveal internals such as scopes, wallet balance or upstream service names. Give a reference code (your own ticket id, optionally with the StampMitra requestId) only where a support conversation is likely.
A note on negative results versus errors
An error is different from a negative verification result. According to the reference, a response that says the PAN is not valid, or a record is not found, is still a successful, completed and billable call with a 200 and a referenceId. The success shapes show the fields: valid on PAN and GSTIN responses, found on Udyam and the PAN-to-GSTIN lookup, and accountExists on bank accounts. Do not show "something went wrong" for these; show a business outcome such as "This PAN could not be verified" and route the case according to your own policy. Mixing up the two is a common reason users see a scary error when the system worked.
Internationalisation
If you serve users in more than one language, key your messages by error class, not by the English message. That way the translation does not depend on server wording, which could change.
What should you log, and what must you never log?
Good error logs let you answer three questions in minutes: which request failed, why, and how many users were affected. Bad logs either say nothing or contain data you should not retain.
Fields to log on every failure
requestIdfrom the error body (the most important field).- HTTP status and
error.code. - Endpoint path and method, for example
POST /v1/verify/pan. - Environment (sandbox or production) and the key's label or prefix, never the full key.
- Attempt number and total elapsed time, so you can see retry behaviour.
- Your own correlation id, such as a user session or job id.
- Latency of the call.
- The success-side
referenceId(formatSMV-YYYYMMDD-XXXXXXX) for calls that did succeed, so you can match your records to StampMitra's transaction ids.
Fields to never log
Never log the API key or the Authorization header. Do not log full PAN, GSTIN, bank account, driving licence or passport values, or personal details from responses such as owner names and addresses. If you need to correlate, log a salted hash or a masked form (last four characters). Keep the full request body out of general application logs, because those logs often have broader access and longer retention than the data deserves. These are general good practices, and they line up with the data-minimisation and purpose-limitation ideas in India's Digital Personal Data Protection Act 2023. Check the Act's text and take advice from qualified counsel for your obligations.
A structured log line
{
"level": "warn",
"event": "stampmitra.api_error",
"status": 429,
"code": "TOO_MANY_REQUESTS",
"requestId": "a1b2c3d4-e5f6-4a1b-8c2d-9f0e1a2b3c4d",
"method": "POST",
"path": "/v1/verify/pan",
"env": "production",
"attempt": 2,
"elapsedMs": 2310,
"correlationId": "job_2026-10-16_nightly"
}Log levels
Use info for successful calls (sampled if volume is high), warn for retried failures such as 429 and 502, and error for terminal failures that need a person: 401, 402, 403, and any exhausted retry budget. Use debug for 400 and 404 when they are user-caused, or you will drown real problems in typos.
How should you alert on API errors?
Alert on conditions that need action, not on every non-200. A rule set that works for most teams uses error class, rate and duration.
| Signal | Condition (example thresholds) | Severity | First responder action |
|---|---|---|---|
| 401 in production | Any, or more than a handful in 5 minutes | Page | Check that the secret was deployed and not rotated |
| 403 | First occurrence after a release, then dedupe | Page or ticket | Compare required scope to key scopes |
| 402 | Any | Page to wallet owner | Top up, then resume paused jobs |
| 429 | Above 5 percent of calls for 10 minutes | Ticket, page if rising | Lower concurrency, review batching |
| 502 | Above 2 percent of calls for 5 minutes | Page | Check the status page, enable degrade mode |
| 400 | Sudden jump versus baseline | Ticket | A deploy broke input shaping, or a form regressed |
| Exhausted retries | Any | Ticket | Review the logs by requestId |
These percentages are examples to tune against your own baseline, not StampMitra guidance. The point is to alert on rates over windows for the retryable errors, and on first occurrence for credential and wallet errors.
Dashboards worth building
A single panel with calls by status code over time, one with p95 latency per endpoint, one with retry attempts per call, and one with the count of calls that ended in a terminal error will cover most investigations. Add a panel for 402 and 429 together, since they often move with the same batch jobs.
Runbook links in the alert
Put a link to your internal runbook and to the StampMitra status page in every alert body. Whoever is on call at 3 a.m. should not have to remember where the status page lives.
Synthetic checks
Run a scheduled sandbox call to a cheap endpoint, such as a verification with a known sample value, every few minutes. If it fails while real traffic is quiet, you learn about a credential or network problem before customers do. Remember that sandbox has a lifetime call quota per the platform copy, so keep the probe frequency low enough that it does not consume the quota you need for testing.
How do you build a reusable error-handling module in Node.js and Python?
Node.js version
The module below centralises everything above: one parser for the envelope, a typed error class, a retry policy that only retries 429 and 502 (plus network failures), exponential backoff with jitter, and a deadline. It uses the built-in fetch available in current Node.js versions. Base URL, path, headers and body match the reference: https://api-b2b.stampmitra.in, POST /v1/verify/pan, a Bearer key and a JSON body with pan and optional name.
// stampmitra-client.js
const BASE_URL = "https://api-b2b.stampmitra.in";
export class StampMitraError extends Error {
constructor({ status, code, message, requestId, retryable, attempts }) {
super(message);
this.name = "StampMitraError";
this.status = status;
this.code = code;
this.requestId = requestId;
this.retryable = retryable;
this.attempts = attempts;
}
// Safe, user-facing text keyed by class, never by server wording.
get userMessage() {
switch (this.status) {
case 400: return "Please check the details you entered and try again.";
case 404: return "We could not find that record.";
case 429: return "We are very busy. Please try again in a minute.";
case 502: return "The verification service is temporarily unavailable. We will retry shortly.";
default: return "We could not complete this check right now. Please try again later.";
}
}
}
const RETRYABLE = new Set([429, 502]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const backoff = (n, base = 500, cap = 30000) =>
Math.min(cap, base * 2 ** (n - 1)) * (0.5 + Math.random() / 2);
async function parseError(res) {
try {
const body = await res.json();
const e = body && body.error ? body.error : {};
return { code: e.code || "UNKNOWN", message: e.message || res.statusText, requestId: e.requestId };
} catch {
return { code: "NON_JSON_ERROR", message: res.statusText, requestId: undefined };
}
}
export async function callStampMitra(path, body, {
apiKey = process.env.STAMPMITRA_API_KEY,
maxAttempts = 4,
deadlineMs = 15000,
logger = console,
} = {}) {
const started = Date.now();
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch(BASE_URL + path, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10000),
});
} catch (netErr) {
// Timeout or connection failure: no response was read. Retry within budget.
if (attempt < maxAttempts && Date.now() - started < deadlineMs) {
await sleep(backoff(attempt));
continue;
}
throw new StampMitraError({ status: 0, code: "NETWORK_ERROR", message: String(netErr.name), retryable: true, attempts: attempt });
}
if (res.ok) return res.json();
const { code, message, requestId } = await parseError(res);
const retryable = RETRYABLE.has(res.status);
logger.warn(JSON.stringify({
event: "stampmitra.api_error", status: res.status, code, requestId,
path, attempt, elapsedMs: Date.now() - started,
}));
const budgetLeft = attempt < maxAttempts && Date.now() - started < deadlineMs;
if (retryable && budgetLeft) {
await sleep(backoff(attempt));
continue;
}
throw new StampMitraError({ status: res.status, code, message, requestId, retryable, attempts: attempt });
}
}Using the Node.js module
import { callStampMitra, StampMitraError } from "./stampmitra-client.js";
try {
const r = await callStampMitra("/v1/verify/pan", { pan: "ABCDE1234F", name: "Rahul Kumar" });
console.log(r.referenceId, r.valid, r.panStatus);
} catch (e) {
if (!(e instanceof StampMitraError)) throw e;
if (e.status === 402) await pauseFeatureAndAlertWalletOwner(e.requestId);
if (e.status === 401 || e.status === 403) await alertOnCall(e.status, e.requestId);
showToUser(e.userMessage);
}Design notes
The module never retries a 4xx other than 429, so it cannot loop on bad input or a bad key. The network-failure branch retries because no response was read, which is the ambiguous case discussed in the 502 section, so keep maxAttempts small and, for order or payment creation, send an Idempotency-Key. The jitter keeps simultaneous workers from synchronising. For production use, add a circuit breaker around 402 and a concurrency limiter in front of callStampMitra.
Python version
The Python version mirrors the Node.js one so a polyglot team has the same behaviour everywhere. It uses requests with an explicit timeout, a typed exception, and the same retry rules.
# stampmitra_client.py
import os, time, random, json, logging
import requests
BASE_URL = "https://api-b2b.stampmitra.in"
RETRYABLE = {429, 502}
log = logging.getLogger("stampmitra")
class StampMitraError(Exception):
def __init__(self, status, code, message, request_id=None, retryable=False, attempts=1):
super().__init__(message)
self.status = status
self.code = code
self.request_id = request_id
self.retryable = retryable
self.attempts = attempts
@property
def user_message(self):
return {
400: "Please check the details you entered and try again.",
404: "We could not find that record.",
429: "We are very busy. Please try again in a minute.",
502: "The verification service is temporarily unavailable. We will retry shortly.",
}.get(self.status, "We could not complete this check right now. Please try again later.")
def _backoff(attempt, base=0.5, cap=30.0):
return min(cap, base * 2 ** (attempt - 1)) * (0.5 + random.random() / 2)
def _parse_error(resp):
try:
err = resp.json().get("error", {})
return err.get("code", "UNKNOWN"), err.get("message", resp.reason), err.get("requestId")
except ValueError:
return "NON_JSON_ERROR", resp.reason, None
def call_stampmitra(path, body, api_key=None, max_attempts=4, deadline_s=15.0):
api_key = api_key or os.environ["STAMPMITRA_API_KEY"]
started = time.monotonic()
attempt = 0
while True:
attempt += 1
within_budget = attempt < max_attempts and time.monotonic() - started < deadline_s
try:
resp = requests.post(
BASE_URL + path,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
data=json.dumps(body),
timeout=10,
)
except (requests.Timeout, requests.ConnectionError) as exc:
if within_budget:
time.sleep(_backoff(attempt))
continue
raise StampMitraError(0, "NETWORK_ERROR", type(exc).__name__, retryable=True, attempts=attempt)
if resp.ok:
return resp.json()
code, message, request_id = _parse_error(resp)
retryable = resp.status_code in RETRYABLE
log.warning(json.dumps({
"event": "stampmitra.api_error", "status": resp.status_code, "code": code,
"requestId": request_id, "path": path, "attempt": attempt,
"elapsedMs": int((time.monotonic() - started) * 1000),
}))
if retryable and within_budget:
time.sleep(_backoff(attempt))
continue
raise StampMitraError(resp.status_code, code, message, request_id, retryable, attempt)Using the Python module
from stampmitra_client import call_stampmitra, StampMitraError
try:
r = call_stampmitra("/v1/verify/pan", {"pan": "ABCDE1234F", "name": "Rahul Kumar"})
print(r["referenceId"], r["valid"], r["panStatus"])
except StampMitraError as e:
if e.status == 402:
pause_feature_and_alert_wallet_owner(e.request_id)
elif e.status in (401, 403):
alert_on_call(e.status, e.request_id)
show_to_user(e.user_message)Testing the module without spending quota
Do not test retry logic against the live API. Point BASE_URL at a local stub that returns canned bodies copied from the reference (a 429, then a 502, then a 200) and assert on the number of attempts and the final result. Unit-test the no-retry paths for 400, 401, 402, 403 and 404, because a regression there is silent until production. Because the exception carries request_id, a failing test can print the exact value you would send to support.
How do you test error handling in sandbox and move to production?
Sandbox gives you a free place to prove your handling before real money and real customers are involved. According to the platform copy, sandbox keys start with sk_test_, sandbox calls are metered and logged but never debit your wallet, and there is a lifetime sandbox call quota. The authentication docs say production keys (sk_live_) require a KYC-approved organisation. The quickstart takes you to a first sandbox call, and the sandbox versus production keys guide goes deeper on the differences.
Which errors you can trigger deliberately
| Error | Safe way to trigger in sandbox | What to verify |
|---|---|---|
| 400 | Send a PAN such as ABCDE123F, or omit a required field | No retry, field-level user message, no PAN in logs |
| 401 | Send a deliberately wrong key | No retry, high-severity alert fires |
| 403 | Call an endpoint your test key's scope does not cover | No retry, alert text names the endpoint |
| 404 | Request an order id that does not exist | Not-found state shown to the user |
| 429 | Use a local stub, because hammering the real API spends your lifetime quota | Backoff timing, attempt cap |
| 402, 502 | Use a local stub, because they depend on wallet and upstream state | Circuit breaker opens, degrade mode works |
Do not intentionally exhaust the sandbox quota just to see a 429. It is a lifetime quota, so you would be removing your own ability to test later.
Production readiness checklist
- One module owns all HTTP calls to StampMitra, with the status policy in the decision table.
- Keys come from a secret store, differ per environment, and have only the scopes each service needs.
- Every failure logs
requestId, status, code, path and attempt, and never logs keys or raw identifiers. - Alerts exist for 401, 402, 403, sustained 429, and sustained 502, with a runbook link.
- A low-balance alert fires before the fulfilment wallet reaches zero.
- Concurrency is capped per endpoint, and bulk jobs are throttled.
- User messages exist for each error class, and negative verification results have their own business-outcome wording.
- Retries on order and payment creation use
Idempotency-Keyafter you have confirmed its behaviour in sandbox. - You know where the status page and support are, and who on your team may contact them.
- You have re-read the API acceptable use policy and make sure your retry behaviour stays well within it.
After go-live
Compare your own count of successful verification calls with the usage shown in the dashboard for the first few days. A mismatch points to retry behaviour, an ambiguous timeout, or a logging gap, and it is far cheaper to catch in week one than in the first invoice.
How do you handle an incident with the status page and support?
When errors spike, the first job is to decide whether the problem is yours, a configuration issue, or on StampMitra's side. The status route on this site points to the hosted page at status.stampmitra.in, which carries live status and incident history for the API, dashboard and docs, according to the site's status page copy.
A five-minute triage
- Read the pattern. Which statuses are rising? All 502 across endpoints suggests upstream. All 401 right after a deploy suggests your secret. A wall of 429 right after a batch started suggests your own volume. A single 403 suggests a scope change.
- Check the status page. If there is an open incident for the API, switch to degrade mode, post an internal note, and avoid retry storms. Do not raise a ticket for something already announced unless your impact differs.
- Check your side. Recent deploys, rotated secrets, changed batch sizes, a new product you started calling without the scope or entitlement.
- Collect evidence. Gather three to five failing
requestIdvalues with timestamps in UTC, the endpoint, the environment, and the status and code returned. - Contact support if the status page is green and the failure continues, or if the issue is specific to your organisation.
How to contact support
Email [email protected] or raise a ticket from the support page, which links the support portal; the contact page lists other ways to reach the team. This is the only support address for the developer platform, so use it rather than guessing at others. A good ticket reads like a good bug report:
Subject: 502 on POST /v1/verify/pan since 14:05 UTC (production)
Organisation: Example Fintech Pvt Ltd (invented)
Environment: production, key prefix sk_live_ab12
Window: 2026-10-16 14:05 to 14:40 UTC
Pattern: about 40 percent of calls return 502 INTERNAL_ERROR
Sample requestIds:
5c1c1e9e-2f7b-4b7a-9b0e-0f1a2b3c4d5e (14:07:12 UTC)
a1b2c3d4-e5f6-4a1b-8c2d-9f0e1a2b3c4d (14:19:48 UTC)
Retries: 3 attempts with exponential backoff, all failed
Impact: onboarding KYC step paused, applications queuedNever paste the full API key or a customer's identifier numbers into a ticket. The requestId is what the team can trace.
Communication and review
Tell your own stakeholders what customers see, using the user-message mapping rather than technical details. When the incident closes, run a short review: did your alerts fire in time, did the retry budget protect you, did degrade mode work, and which log field was missing? Feed the answers back into the module and the runbook. The API SLA and service availability policy explains how StampMitra frames availability, including that no absolute uptime is promised and that sandbox availability is not necessarily held to production expectations, so design for failure rather than assuming it will not happen.
What are the most common mistakes, and how do you troubleshoot them?
Most integration errors come from a short list of habits. The table maps a symptom to its likely cause and the first thing to check.
| Symptom | Likely cause | First check |
|---|---|---|
| 401 only in production | A sk_test_ key deployed, or secret not set | Print the key prefix (not the key) at startup |
| 403 on a new endpoint | Key scope predates the new product | Compare the endpoint's scope with the key's scopes |
| 400 on a value that looks fine | Hidden whitespace, lower case, or a field name typo | Log string length and a masked value |
| 400 in production, fine in sandbox | Entitlement or environment difference | Check product entitlements, then ask support |
| 429 bursts at the top of the hour | Cron jobs starting together | Stagger jobs, add a limiter |
| Every call 429 in sandbox | Lifetime sandbox quota used up | Check usage in the dashboard, then ask support |
| 502 for a few calls then fine | Transient upstream issue | Confirm retries worked, no action |
| 402 at month end | Bulk run outran the wallet | Top up before batches, add a low-balance alert |
| Duplicate orders after a timeout | Blind retry without Idempotency-Key | Send the key, confirm behaviour in sandbox |
| "Error" shown for a failed PAN match | Negative result treated as an error | Branch on valid or found in a 200 body |
Mistakes in detail
Retrying everything. A generic "retry on any error" wrapper turns a 402 into thousands of identical failures and a 401 into a lockout of your own throughput. Retry only what can change on its own.
Parsing message. A regex over the English text works until the wording is edited. Branch on the status, and log the message.
Ignoring requestId. Teams that log only the status code cannot get a useful answer from anyone, including us. Capture it from the first line of code you write.
Treating a lookup miss as a failure. A negative determination is a completed result. Handle it in your business logic, not in your error handler.
Exposing internals to users. Wallet balances, scopes and upstream names belong in your private logs and runbook.
Assuming undocumented behaviour. Do not code against a Retry-After header, an error details array or idempotent verification calls, because the reference data documents none of them. If you need something the docs do not state, ask support and wait for a written answer.
A short debugging routine
Reproduce with cURL using the same key and body to separate client bugs from API behaviour, read the requestId, compare the status with the decision table, and only then look at the code. If cURL succeeds and your service fails, the difference is almost always in headers, body serialisation or the environment's secret.
How StampMitra Helps
StampMitra is a private LegalTech platform operated by Bani Global Industries LLP. It is not a government agency or a law firm, and nothing in this guide is legal advice. What the developer API does today is narrower and more concrete: one authenticated JSON API, covering identity and business lookups plus e-Stamp services, with a single predictable error envelope that makes the handling in this guide possible.
On the error side, the practical benefits are these. Every failure carries a machine-readable code, a human-readable message and a requestId, and the filter that produces them avoids leaking stack traces or database details. Input is validated up front, so a malformed PAN, GSTIN, vehicle number, CIN or Udyam number fails fast with a field-specific 400 and is not sent for verification. Verification usage is recorded, and the fulfilment wallet debited, only after a successful verification, and sandbox calls do not debit your wallet. Rate limits and scopes are enforced per endpoint and per key, which keeps blast radius small when a credential is misused. The product catalogue lists each endpoint with its scope, request body and documented errors, and the SDKs page, Postman collection and MCP server help you try those calls quickly. If you build with an AI assistant, the build with AI prompts keep the error handling in line with this reference.
For businesses, the same lookups power onboarding flows, as described in the KYC use case for fintech and NBFC teams and the business verification guide for GSTIN, CIN and Udyam. When something goes wrong, the status page and [email protected] are the places to start, and the requestId you logged is the fastest route to an answer. You can create a free sandbox account from signup and rehearse every scenario above before going live; see pricing for current per-call rates and the developer onboarding verification policy for what production access involves.
What is next
These are plans, not features available today, and they may change. We intend to keep expanding the documented error examples as new products ship, and to publish more operational guidance as the platform grows. Webhook delivery for order events is marked as coming soon on the webhooks page; until it is live, poll the order status endpoints rather than waiting for pushes. Anything not described in the reference documentation should be treated as unavailable until StampMitra documents it.
API reference at a glance
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).
Frequently asked questions
What error format does the StampMitra API return?
Every failure uses one JSON envelope with an error object holding code, message and requestId, for example code PAYMENT_REQUIRED with a human-readable message. Branch on the HTTP status first, use the code for metrics, and always log the requestId.
Which StampMitra API errors should I retry?
Retry only 429 (rate limited) and 502 (upstream verification service unreachable), with exponential backoff, jitter, a small attempt cap and a deadline. Do not retry 400, 401, 402, 403, 404 or 409, because the same request will fail the same way.
What does a 402 Payment Required error mean?
A 402 with code PAYMENT_REQUIRED means the fulfilment wallet cannot cover the priced call. Retrying will not help. Alert the wallet owner, pause the feature or queue the work, top up the wallet, and then resume.
What is the difference between a 401 and a 403?
A 401 means the API key is missing or invalid. A 403 means the key is valid but lacks the scope the endpoint requires, such as verification.pan.read. Fix a 401 by correcting the key and a 403 by issuing a key with the right scopes.
Why am I getting a 429 and how long should I wait?
A 429 means you hit a per-endpoint rate limit, or in sandbox possibly the lifetime call quota. No Retry-After header is documented, so use your own exponential backoff with jitter and a capped number of attempts, and reduce concurrency.
Does the StampMitra API support idempotency keys or Retry-After?
The reference documents an optional Idempotency-Key request header only on POST /v1/orders and POST /v1/payments/intents, and it does not document Retry-After. Verification calls are not idempotent, and referenceId is a transaction id, not an idempotency key.
Am I charged when a verification call fails?
The reference says verification usage is recorded and the fulfilment wallet debited only after verification completes successfully. A negative result such as an invalid PAN is still a completed, billable call, while errors like 400 or 502 are failures.
What should I log for each API error?
Log the requestId, HTTP status, error code, endpoint, environment, attempt number, elapsed time and your own correlation id. Never log API keys, the Authorization header, or full PAN, GSTIN, bank account or other personal identifiers.
What should I do if 502 errors continue?
Check the status page at status.stampmitra.in first. If it shows no incident and the failures continue, email [email protected] with the environment, time window in UTC, endpoint and several failing requestId values.
Is a 400 always a validation error?
Mostly yes: verification routes validate input formats up front and return a field-specific message. The reference also labels the 400 as possibly meaning not entitled, so if valid-looking input fails only in production, check entitlements and contact support.
Sources
Verified 2026-10-16 against the StampMitra Developers codebase and published pages. No external websites were used for API facts in this guide.
- StampMitra API reference data, shared error definitions and per-endpoint
errorExamples(src/data/api-products.ts), verified 2026-10-16. - Authentication docs: bearer and
x-api-keyheaders, key prefixes, scopes, rate limits, verified 2026-10-16. - Verification API docs and e-Stamp API docs: endpoint paths, scopes, metering notes, verified 2026-10-16.
- Support page and the status page route (/status, pointing to status.stampmitra.in): contact details and FAQ on 429, verified 2026-10-16.
- API SLA and service availability policy and webhooks page, verified 2026-10-16.
- Digital Personal Data Protection Act 2023: referenced only as general context for data minimisation and purpose limitation; the Act's text was not re-fetched for this draft, so confirm any compliance statement against the official text and with qualified counsel.