Your StampMitra account now tells you three things without anyone opening the dashboard: how much money is left, how much you used, and how close each call came to a rate limit. Read the balance with GET /v1/account/balance, read usage with GET /v1/account/usage, and read the limits from response headers that arrive on your normal verification calls. All of it works with the API key you already use, and none of it changes a response you parse today.
This guide is for the engineer who owns the integration, not the person who tops up the wallet. It explains every field, how requests are counted, which routes carry which header, and a low-balance pattern you can paste into a customer system so a verification flow never fails in front of an end user because the balance ran out overnight.
If you have not made a first call yet, start with the quickstart and the authentication docs, then come back here. The error handling in this guide builds on API errors and handling.
Key takeaways
GET /v1/account/balancereturns the balance in rupees and paise, the low-balance threshold, a state ofOK,LOW_BALANCEorFINISHED, and an estimate of how many calls the balance still covers.GET /v1/account/usagereturns the current and the previous calendar month with requests, billed requests, failed calls that were not billed, spend before GST, a per-product breakdown and a per-day series, all cut on Indian Standard Time days.- Any key that holds at least one verification scope can read its own account. There is no new scope, and a key can never read another organisation's data.
- Live keys receive
X-Wallet-Stateon authenticated calls, plusX-Wallet-Balance-Low: trueonly when the wallet is low. The exact balance is never put in a header, because headers end up in proxy and load balancer logs. - Rate limited routes send
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset, and a 429 carriesRetry-After. Use them to slow down before you hit the wall. - The new headers are optional. Write clients that work when they are missing, because they can be switched off operationally and some routes never send them.
- Sandbox keys get the rate limit headers only. Their balance endpoint answers with a sandbox notice and zero amounts, never a real balance.
- Poll the balance endpoint at a human pace (a minute is plenty). Both endpoints are cached for a few seconds and limited to sixty requests a minute per key.
Why a customer system should watch balance, usage and rate limits
A verification API that bills per successful call has one failure that is entirely predictable: the balance reaches zero. When it does, live calls stop with a 402, and the people who feel it first are your end users in the middle of a signup. The fix is never in code. Somebody has to recharge. What code can do is make sure that somebody finds out days before, and that the flow degrades politely when they do not.
Before these endpoints existed, the only places to see the balance were the dashboard and the invoice emails. That is fine for a founder who checks every morning and poor for a company with an on-call rota. A balance you can read over the API can feed a metric, a Slack alert, a status banner inside your own admin panel, or a feature flag that parks non-urgent work.
Usage matters for a different reason. Finance wants to know spend before GST per product per day, support wants to know why a customer saw failures at a given hour, and engineering wants to see whether traffic is growing faster than the plan limit. The usage endpoint answers all three from one response, using Indian calendar days so the numbers match how your accountants think about a month.
Rate limits are the third piece. A limit that only announces itself with a 429 is a limit you discover in production. The headers let a well behaved client watch the remaining allowance and pace itself, which is cheaper than retrying.
What this guide does not cover
Recharging itself, invoices and the dashboard are outside this page. Top-ups are described in the wallet docs and on the pricing page, and webhooks for order events are on the webhooks page. This page is about reading state, not changing it.
What is new and what did not change
Everything described here is additive. No existing endpoint, body field, status code, error code or header changed. A client written last month keeps working byte for byte.
Two read-only endpoints are new: GET /v1/account/balance and GET /v1/account/usage. A small set of response headers is new on authenticated calls: the three X-RateLimit-* headers, Retry-After on a 429 that had none, X-Wallet-State and X-Wallet-Balance-Low. That is all.
Two consequences follow. First, you do not have to change anything to keep working. Second, if you want the new information you add code that reads it, and that code should treat every new value as optional. The response bodies of verification calls are identical with and without the headers, so a strict JSON parser is unaffected.
Existing endpoints that look similar keep their own behaviour. GET /v1/usage/summary still returns the billing period totals and the calls by service. GET /v1/api-credit-wallet still returns the wallet record. The new endpoints sit next to them and answer different questions: spend and state instead of raw counts.
Versioning
The published OpenAPI document moved from 1.0.0 to 1.1.0, a minor bump, because the change is a backwards compatible addition. Generated clients pick up two new operations named getAccountBalance and getAccountUsage. You can download the document from /openapi.json or import the Postman collection, which includes both requests.
Authentication, scopes and who can read what
Both endpoints use the same API key authentication as every other /v1 route. Send Authorization: Bearer sk_live_... or the x-api-key header, exactly as described in the authentication docs.
The scope rule is deliberately light. A key that holds any scope starting with verification. can read its own organisation's balance and usage. A key that holds an e-Stamp scope can do the same, because e-Stamp calls are paid from the same wallet. A key with only unrelated scopes, such as orders.read or payments.read, receives a 403 with the code INSUFFICIENT_SCOPE. A missing or wrong key is a 401, as everywhere.
Isolation between organisations
The organisation and the environment always come from the key, never from the request. If you add ?organizationId=... or a header that names a different organisation, it is ignored. A key can read exactly one organisation's data, its own. A live key sees live usage only, and a sandbox key sees sandbox usage only. The tests behind these endpoints run a key for one organisation against data for another and assert that nothing leaks, which is the property you should expect from a billing read.
Why no new scope
Adding a scope would have forced every existing key to be re-issued before anyone could see a balance. Reading your own balance does not widen what a key can do to anyone else, so the existing verification scopes are enough. If your security policy wants a key that can read the account and nothing else, say so through support; today the rule is any verification scope.
GET /v1/account/balance, field by field
The balance response is one JSON object. Money is always a pair, paise as an integer and rupees as a number, so you can do arithmetic on paise and display rupees without rounding mistakes.
{
"generatedAt": "2026-10-30T10:15:00.000Z",
"environment": "LIVE",
"sandbox": false,
"notice": null,
"currency": "INR",
"balance": { "paise": 125050, "rupees": 1250.5 },
"available": { "paise": 125050, "rupees": 1250.5 },
"state": "OK",
"lowBalance": false,
"lowBalanceThreshold": { "paise": 50000, "rupees": 500 },
"estimatedCallsRemaining": {
"atCheapestPrice": 312,
"cheapestProduct": { "serviceCode": "bank-account.verify", "name": "Bank account verification", "price": { "paise": 400, "rupees": 4 } },
"atSevenDayAverage": 142,
"sevenDayAverageCost": { "paise": 880, "rupees": 8.8 }
},
"lastTopUp": { "amount": { "paise": 100000, "rupees": 1000 }, "at": "2026-10-21T08:00:00.000Z" },
"signupCredit": { "granted": { "paise": 10000, "rupees": 100 }, "left": { "paise": 0, "rupees": 0 } }
}| Field | Meaning |
|---|---|
generatedAt | When this snapshot was computed. It can be up to five seconds old. |
environment, sandbox | LIVE for an sk_live_ key, SANDBOX for an sk_test_ key. |
notice | Text for sandbox keys, null for live keys. |
balance | Cash in the wallet. Includes any signup credit that has not been spent. |
available | What a call can spend right now: cash plus eligible promotional credit. Equal to balance when you have no promotional credit. |
state | OK, LOW_BALANCE or FINISHED. |
lowBalance | true whenever state is not OK. A convenient boolean for dashboards. |
lowBalanceThreshold | The amount under which the state becomes LOW_BALANCE. Five hundred rupees by default. |
estimatedCallsRemaining | How far the balance goes. See the section on estimates below. |
lastTopUp | The newest recharge: the base amount credited, GST excluded, and when. null if you never recharged. |
signupCredit | The signup credit granted and the part of it still left in the balance. |
Amounts and GST
Every amount in these endpoints is before GST. The wallet is debited the price of the product, and GST is charged when you recharge, not on each call. That is why the top-up amount shown in lastTopUp is the base amount credited and not the total you paid including tax. Your invoice carries the GST breakdown.
The signup credit
New accounts receive a small signup credit that sits in the cash balance like any other money. The ledger spends it first, because it was credited first. signupCredit.left is the part of that credit still unspent, bounded by the balance. Once it reaches zero it stays zero, and a top-up never brings it back. If an account never received the credit, granted is zero.
Caching
The snapshot is cached in StampMitra for five seconds per organisation. Two requests inside that window return the same generatedAt. Recharging and seeing the new number takes at most a few seconds. The response also carries Cache-Control: private, no-store, which tells your own proxies and browsers not to keep a copy.
The three states: OK, LOW_BALANCE and FINISHED
The state is the field most integrations will actually use. It is computed on the server so every client sees the same verdict.
OK means the available amount is at or above the low-balance threshold. LOW_BALANCE means it is below the threshold but at least as large as the cheapest call you could make, so calls still succeed. FINISHED means the available amount is zero or smaller than the cheapest approved call, so a live call would be refused with a 402 and the message asking you to recharge.
| State | Available amount | What live calls do | What you should do |
|---|---|---|---|
OK | at or above the threshold | succeed | nothing |
LOW_BALANCE | below the threshold, still above the cheapest call | succeed | alert the wallet owner, recharge soon |
FINISHED | zero, or below the cheapest call | fail with 402 | recharge now, pause or queue work |
Where the threshold comes from
The default threshold is five hundred rupees. For accounts whose cheapest approved product is unusually expensive, the threshold is raised to ten times that product's price, so the warning always arrives while there is room for several calls. You read the effective number from lowBalanceThreshold, never hard-code it.
The state matches the dashboard
The same three states drive the wallet chip in the dashboard header and the low-balance email. The names differ slightly because the dashboard says READY and NEEDS_RECHARGE where the API says OK and FINISHED. They describe the same condition.
Calls remaining: the cheapest price and the seven day average
A balance in rupees does not tell an engineer how many verifications remain. The estimatedCallsRemaining block turns money into calls in two ways.
atCheapestPrice divides the available amount by the price of the cheapest product you are approved to use. It is the optimistic figure: if you ran nothing but that product, this many calls fit. cheapestProduct names the product and its price so you can show the assumption.
atSevenDayAverage divides the available amount by the average billed price per call over the last seven days of live traffic, so it reflects your real product mix. sevenDayAverageCost shows that average. If you have no billed calls in the last seven days, both are null and you should fall back to the cheapest figure.
Reading the two numbers together
Treat the average based figure as the planning number and the cheapest figure as the ceiling. A big gap means your traffic uses expensive products. A shrinking average figure over a week means either rising volume or a shift towards costlier lookups. Neither number accounts for a change in price, and neither accounts for traffic you have not made yet. They are estimates, labelled as such.
When the figures are null
atCheapestPrice is null when no product is approved and priced for the organisation, for example an account that has not finished production onboarding. The endpoint never invents a price. If you see null, display the balance without the estimate.
GET /v1/account/usage, field by field
The usage response carries two month blocks, a block for today and the limits that apply to you. Month blocks share one shape, so one function in your code can render both.
{
"generatedAt": "2026-10-30T10:15:00.000Z",
"environment": "LIVE",
"sandbox": false,
"notice": null,
"timezone": "Asia/Kolkata",
"currentMonth": {
"month": "2026-10",
"from": "2026-09-30T18:30:00.000Z",
"to": "2026-10-30T10:15:00.000Z",
"gstIncluded": false,
"requests": 1840,
"billedRequests": 1712,
"failedNotBilled": 101,
"successNotBilled": 0,
"pendingBilling": 27,
"spend": { "paise": 684800, "rupees": 6848 },
"byProduct": [
{ "serviceCode": "pan.verify", "name": "PAN verification", "requests": 1500, "billedRequests": 1400, "failedNotBilled": 90, "successNotBilled": 0, "pendingBilling": 10, "spend": { "paise": 560000, "rupees": 5600 } }
],
"daily": [
{ "date": "2026-10-29", "requests": 120, "billedRequests": 112, "failedNotBilled": 8, "successNotBilled": 0, "pendingBilling": 0, "spend": { "paise": 44800, "rupees": 448 } }
]
},
"previousMonth": { "month": "2026-09", "...": "same shape as currentMonth" },
"today": {
"date": "2026-10-30",
"requests": 64,
"billedRequests": 60,
"failedNotBilled": 4,
"spend": { "paise": 24000, "rupees": 240 },
"requestsPerDay": { "limit": null, "used": 64, "remaining": null }
},
"limits": {
"requestsPerMinute": { "limit": 60, "source": "PLAN" },
"requestsPerDay": { "limit": null, "source": "NONE" }
}
}The byProduct array is sorted with the busiest product first. The daily array is zero filled: every Indian calendar day appears, including days with no traffic, so a chart does not need to guess at gaps. For the current month the series runs from day one to today. For the previous month it covers every day.
What the numbers are based on
Usage comes from the request record that StampMitra keeps for calls on the metered product routes: verification, e-Stamp and e-Sign. Each call that reached the API with a valid key is recorded once, including calls that were stopped before reaching the provider, such as a 402 or a 429. The account endpoints themselves are not counted, so polling does not inflate your usage.
Retention
Call records stay in the database for at least ninety days, so the current and the previous month are always complete. The usage endpoint deliberately covers only those two months. For older history use your invoices and the request log endpoint.
How requests are counted: billed, failed and pending
Every counted call falls into exactly one of four groups, and the groups add up to the total. That is the property finance teams usually test first, so it is guaranteed.
| Group | Rule |
|---|---|
billedRequests | The call was billed. This includes the rare quotation rule that bills a particular failed outcome. |
pendingBilling | The debit is waiting to be applied and will settle shortly. Its amount is not in spend yet. |
failedNotBilled | The HTTP status was 400 or higher and the call was not billed. Rejections before the provider are included. |
successNotBilled | The HTTP status was below 400 and the call was not billed, for example a sandbox call or an unpriced product. |
So requests = billedRequests + pendingBilling + failedNotBilled + successNotBilled.
Why failed calls are not billed
Verification is billed on success. A malformed PAN that fails validation, a call refused because the balance was empty, a call stopped by the rate limiter and a call where the upstream service was unreachable are all not billed. They appear in failedNotBilled so you can see how much traffic produced no result. A high share is worth investigating: it usually means your inputs are not validated before sending, or a limit is being hit.
Spend
spend is the sum of the amounts of the billed calls, before GST. It does not include pending debits. A day that shows pending calls will show slightly higher spend when you read it again after they settle.
Reconciling with invoices
Spend here is price book spend, the quantity that moves the wallet. Invoices are issued on recharges and plan fees and carry GST, so a month of usage spend will not equal a month of invoices. Use usage for engineering and operations questions and invoices for tax.
Indian days, months and why the boundary matters
Day and month boundaries use Indian Standard Time, UTC plus five thirty, which has no daylight saving. A call made at 23:59:59 IST on the thirtieth belongs to the old month. A call at 00:00:00 IST on the first belongs to the new one. In UTC terms, midnight IST is 18:30 the evening before.
That detail decides which month a late night call lands in, so the response shows the instants used. The from value of a month is its first moment in UTC, for example 2026-09-30T18:30:00.000Z for October. The timezone field says Asia/Kolkata in every response.
Month rollover and year end
The endpoint always means the calendar month that contains the current Indian moment. In the first hours of a new month in India it is still the previous month in UTC, and the endpoint already reports the new month. The previous month block rolls with it, and across a year end the previous month of January is December of the year before.
Today
The today block is the current Indian day only. Use it for a daily budget alarm. It is not a rolling twenty four hours.
A tip for charts
Use the date strings from the daily array as they are. Parsing them into a local timezone in a browser can shift a day by one in some timezones. Display them as plain dates.
Rate limit headers
Routes that go through the rate limiter send three headers on every response, successful or not.
| Header | Value |
|---|---|
X-RateLimit-Limit | The number of requests allowed in the current one minute window. |
X-RateLimit-Remaining | How many requests are left in that window after this one. Never below zero. |
X-RateLimit-Reset | Whole seconds until the window ends and the counter starts again, between one and sixty. |
The numbers describe a fixed window aligned to the clock minute, so the reset value falls as the minute goes on and jumps back at the next boundary. Remaining counts down, and a request that arrives with remaining at zero is the one that fails.
Which limit is reported
A call can pass through two counters: a limit of the route itself and the limit of your plan, which is shared by all your keys and all verification routes. When both apply, the headers describe the one with less room left, because that is the one that will bite first. On the verification routes the limit is your plan's requests per minute, which is also what the usage endpoint reports under limits.requestsPerMinute.
Retry-After
When the rate limiter rejects a call with a 429, the response also carries Retry-After, in seconds, equal to the reset value. If a response already had a Retry-After, the existing value is kept untouched. A 429 that does not come from the rate limiter, for example a monthly cap on a newly opened account, does not get an invented Retry-After.
Using the headers
A client that only reacts to 429 loses a request every time it bumps the ceiling. A client that reads X-RateLimit-Remaining can slow its worker pool when the number drops under a margin, say ten percent of the limit, and resume after the reset. The pattern is shown in the code samples below.
Wallet headers: X-Wallet-State and X-Wallet-Balance-Low
Live keys receive wallet headers on authenticated calls.
X-Wallet-State carries OK, LOW_BALANCE or FINISHED, the same value the balance endpoint reports. X-Wallet-Balance-Low is sent with the value true only when the wallet is low, meaning the state is LOW_BALANCE or FINISHED. It is never sent as false: its absence means not low.
The point of the headers is zero cost awareness. Every verification response already crosses your code, so noticing a low wallet there is free, with no extra call. A header can raise the alert within the same minute that the balance crosses the line.
The exact balance is never in a header
This was a deliberate decision. Headers are copied into reverse proxy logs, load balancer logs, observability tools and sometimes browser tools. A balance in a header would leak a commercially sensitive number to every system on the path. The state reveals only what a client needs. When you want the amount, call the balance endpoint over TLS from a trusted backend.
Headers can be missing
The wallet state is kept in the memory of the StampMitra servers and refreshed from the balance the metering step already reads. For a verification call it is available straight away. On other routes, the first call of an organisation on a freshly started server may not carry it, because StampMitra will not delay your response to fetch it. Treat the wallet headers as hints. When they are absent, do nothing; when you need certainty, call the balance endpoint.
Which routes carry which headers
The headers are added to authenticated calls under /v1, where authentication means a valid key. The table lists what to expect.
| Call | X-RateLimit-* | Retry-After | X-Wallet-State and X-Wallet-Balance-Low |
|---|---|---|---|
All POST /v1/verify/* routes, live key | yes, plan limit | on 429 | yes when known |
All POST /v1/verify/* routes, sandbox key | yes, sandbox limit | on 429 | no |
GET /v1/account/balance and /usage, live key | yes, 60 a minute | on 429 | yes when known |
GET /v1/account/balance and /usage, sandbox key | yes, 12 a minute | on 429 | no |
| Routes with their own limit, such as wallet top-up | yes, that limit | on 429 | yes when known, live keys |
| Authenticated routes without a rate limiter | no | no | yes when known, live keys |
| Any call with a missing or invalid key (401) | no | no | no |
Error responses count too
The headers are written when the response starts, so they accompany errors as well. A 402 for an empty wallet carries X-Wallet-State: FINISHED next to the unchanged 402 body. A 429 carries the reset information. A 400 for a malformed PAN carries the same rate limit information as a 200 would have.
What never changes
Bodies, status codes, error codes and every existing header are exactly as before. If a response has a body of a given length without the new headers, it has the same body with them.
The switch
StampMitra can switch the extra headers off without a deployment. When they are off, no response carries them, while the two new endpoints keep working. This exists as a safety valve for the rare case that an intermediary misbehaves with unfamiliar headers. It is one more reason to code the headers as optional.
Sandbox keys
A sandbox key never sees a real balance. GET /v1/account/balance with an sk_test_ key returns environment: "SANDBOX", a sandbox notice, state OK and zero for every amount. No wallet is read for the request at all. The reason is simple: sandbox calls are free and return sample data, so there is nothing to spend.
GET /v1/account/usage with a sandbox key counts your sandbox calls, with spend always zero and the notice explaining that these are sandbox calls. A live key never sees sandbox calls, and a sandbox key never sees live calls, even though both belong to one organisation.
Rate limits in sandbox
Sandbox keys get a fifth of the production limit of a route, with a minimum of one request per window. For the account endpoints that is twelve requests a minute against sixty for live keys. Verification, e-Sign and e-Stamp calls with a sandbox key are limited separately, to 10 requests a minute and 200 a day per organisation. Every e-Stamp sandbox request counts toward that allowance, catalogue reads, order and stamp lookups and the two sandbox wallets included, and the sample-certificate download link has its own cap of 60 an hour per IP address. The usage endpoint reports both under limits.requestsPerMinute and limits.requestsPerDay, each with the source SANDBOX, and today.requestsPerDay.used shows how many of today's 200 are used. The day resets at midnight IST.
Wallet headers in sandbox
Sandbox responses carry the rate limit headers and nothing about the wallet. If your client sees a wallet header, it is talking to production. Some teams use that as a guard in tests: a test that finds X-Wallet-State in a response has accidentally used a live key.
A recommended low-balance pattern for customer systems
The goal is to have the people who can recharge hear about it early, and to let the system degrade in a controlled way if they do not. Four layers work well together.
Layer one: a scheduled check
Run a small job every five minutes. It calls GET /v1/account/balance, exports state and the available paise as metrics, and compares the state with the one it saw last time. It alerts on transitions, not on every run: OK to LOW_BALANCE raises a warning to the finance or platform channel, LOW_BALANCE to FINISHED raises a page. When the state returns to OK after a recharge it posts a resolved message, so a human knows the fix worked.
Layer two: read the header on the hot path
In the code that already calls verification, read X-Wallet-State from each response and store the latest value in a shared variable or cache key. If you see FINISHED or LOW_BALANCE, trigger the alert logic early instead of waiting for the next scheduled check. This is a few lines and has no cost.
Layer three: a circuit for non essential work
Divide your verification work into critical and deferrable. A customer waiting at a signup screen is critical. A nightly re-verification of old records is deferrable. When the state is LOW_BALANCE, keep the critical flows and pause the deferrable ones. When the state is FINISHED, queue the critical requests that can wait, show your user a clear message for those that cannot, and stop sending calls that will certainly fail.
Layer four: a runway estimate
Compute days left as the available paise divided by the average daily spend from the usage endpoint. Alert at seven days, three days and one day, regardless of the state. A wallet that is OK today but burns through a day of traffic every day will not stay OK, and a runway alert fires earlier than the threshold does.
| Signal | Threshold | Action |
|---|---|---|
| Runway | seven days | notify finance with the suggested recharge |
state becomes LOW_BALANCE | any | warn the owner, pause deferrable jobs |
state becomes FINISHED | any | page, queue critical work, show a friendly message |
state returns to OK | any | resolve the alert, resume paused jobs |
Avoid alert fatigue
Alert on state changes and keep a cool down of an hour per level. The balance endpoint is cheap but a flapping balance around the threshold, with top-ups and heavy traffic, can toggle the state. A small hysteresis such as requiring the state to hold for two consecutive checks before resolving keeps the channel quiet.
Recharge ownership
Name an owner. A low balance alert addressed to a channel with twenty people is read by nobody. Put one person and one backup in the alert text, and include the link to recharge from your dashboard. Support can help if your finance process needs a payment link or an invoice first; see support.
cURL examples
Read the balance with a live key:
curl -s https://api-b2b.stampmitra.in/v1/account/balance \
-H "Authorization: Bearer $STAMPMITRA_LIVE_KEY"Read usage and pull the spend of the current month with jq:
curl -s https://api-b2b.stampmitra.in/v1/account/usage \
-H "Authorization: Bearer $STAMPMITRA_LIVE_KEY" \
| jq '{month: .currentMonth.month, requests: .currentMonth.requests, spendRupees: .currentMonth.spend.rupees}'Show the headers of a verification call, without printing the body, to see the limit and the wallet state:
curl -s -o /dev/null -D - https://api-b2b.stampmitra.in/v1/verify/pan \
-H "Authorization: Bearer $STAMPMITRA_LIVE_KEY" \
-H "Content-Type: application/json" \
-d '{"pan":"ABCDE1234F"}' | grep -i -E "^(x-ratelimit|x-wallet|retry-after)"A typical result looks like this, where the wallet line appears only for live keys and only when known:
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
x-ratelimit-reset: 34
x-wallet-state: OKA shell check for cron
This one line fails with a non zero exit code when the wallet is not OK, which suits a cron job or a CI health step:
curl -s https://api-b2b.stampmitra.in/v1/account/balance -H "Authorization: Bearer $STAMPMITRA_LIVE_KEY" | jq -e '.state == "OK"' > /dev/nullNode.js example
The module below reads the balance, classifies the state, and exposes a pacing helper that uses the rate limit headers. It uses the built-in fetch of Node 18 and later, with no dependencies.
const BASE = "https://api-b2b.stampmitra.in";
const KEY = process.env.STAMPMITRA_LIVE_KEY;
async function getBalance() {
const res = await fetch(`${BASE}/v1/account/balance`, { headers: { Authorization: `Bearer ${KEY}` } });
if (!res.ok) throw new Error(`balance ${res.status}`);
return res.json();
}
let lastState = "OK";
/** Called by a scheduler every five minutes. Alerts only on a change of state. */
async function checkWallet(notify) {
const b = await getBalance();
if (b.state !== lastState) {
await notify({
level: b.state === "FINISHED" ? "page" : b.state === "LOW_BALANCE" ? "warn" : "resolved",
text: `Wallet ${lastState} -> ${b.state}. Available Rs ${b.available.rupees}. About ${b.estimatedCallsRemaining.atSevenDayAverage ?? b.estimatedCallsRemaining.atCheapestPrice ?? "unknown"} calls left.`,
});
lastState = b.state;
}
return b;
}
/** Wraps any verification call: reads the optional headers, never depends on them. */
async function verifyPan(pan, onWallet) {
const res = await fetch(`${BASE}/v1/verify/pan`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ pan }),
});
const state = res.headers.get("x-wallet-state");
if (state && state !== "OK") onWallet(state);
const remaining = Number(res.headers.get("x-ratelimit-remaining"));
const limit = Number(res.headers.get("x-ratelimit-limit"));
const resetSeconds = Number(res.headers.get("x-ratelimit-reset"));
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") ?? resetSeconds ?? 5);
await new Promise((r) => setTimeout(r, (wait + Math.random()) * 1000));
return verifyPan(pan, onWallet);
}
if (res.status === 402) throw Object.assign(new Error("wallet empty"), { code: "FINISHED" });
if (limit && remaining < limit * 0.1) await new Promise((r) => setTimeout(r, 250)); // slow down near the ceiling
return res.json();
}
module.exports = { getBalance, checkWallet, verifyPan };Notes on the sample
The retry on 429 is bounded in a real client: add an attempt counter and a deadline, as described in API errors and handling. The sample keeps it short for readability. The notify function is yours, a Slack webhook or a pager integration. The sample never logs the key or the balance headers because the headers do not contain the balance.
Python example
The Python version uses requests. It adds the runway calculation from the usage endpoint, which is the most useful early warning.
import os
import time
import requests
BASE = "https://api-b2b.stampmitra.in"
HEADERS = {"Authorization": f"Bearer {os.environ['STAMPMITRA_LIVE_KEY']}"}
def get_json(path):
r = requests.get(f"{BASE}{path}", headers=HEADERS, timeout=10)
r.raise_for_status()
return r.json()
def runway_days():
"""Days of traffic the available balance covers, from the average daily spend of the current month."""
balance = get_json("/v1/account/balance")
usage = get_json("/v1/account/usage")
days = [d for d in usage["currentMonth"]["daily"] if d["requests"] > 0]
if not days:
return None, balance
average_paise = sum(d["spend"]["paise"] for d in days) / len(days)
if average_paise <= 0:
return None, balance
return balance["available"]["paise"] / average_paise, balance
def verify_pan(pan, retries=3):
for attempt in range(retries + 1):
r = requests.post(f"{BASE}/v1/verify/pan", headers=HEADERS, json={"pan": pan}, timeout=15)
if r.headers.get("X-Wallet-State") in ("LOW_BALANCE", "FINISHED"):
print("wallet is low:", r.headers["X-Wallet-State"]) # replace with your alerting
if r.status_code == 429 and attempt < retries:
time.sleep(int(r.headers.get("Retry-After", "5")) + 0.5)
continue
if r.status_code == 402:
raise RuntimeError("wallet finished: recharge before retrying")
r.raise_for_status()
return r.json()
if __name__ == "__main__":
days, bal = runway_days()
print(bal["state"], "runway days:", None if days is None else round(days, 1))Reading the runway
The function ignores days with no traffic so a quiet weekend does not flatter the average. If you prefer a conservative view use the busiest recent day instead of the mean. Either way, alert on the number of days, not on rupees: seven days of runway means the same thing to every account size.
Handling 429 and 402 correctly
The two errors look similar to a retry loop and require opposite handling.
A 429 from the rate limiter is temporary. Wait for Retry-After, add a little jitter so a fleet of workers does not return in lockstep, cap the attempts, and reduce concurrency. If you see 429 regularly, the better fix is pacing from X-RateLimit-Remaining, or a higher plan limit. The plan limits are listed on the pricing page.
A 402 is not temporary. Retrying a 402 repeats the failure and, worse, floods your logs. Stop, raise the alert, queue what can wait, and resume only after the state returns to OK or LOW_BALANCE. The wallet headers on the 402 response say FINISHED, which gives your code an unambiguous reason that is not a string match on a message.
Do not parse messages
Messages are for people and may be reworded. Use the status code and the code field of the error envelope, and the state fields on the balance endpoint for logic.
The account endpoints can return 429 too
Sixty requests a minute is generous for a monitor and tight for a loop. A job that polls every second will be refused after a minute. Poll every minute or less often.
Caching, polling and the limits of the endpoints themselves
Both endpoints are cheap reads, and they are protected like cheap reads. Each key may make sixty requests a minute to them, twelve for sandbox keys. They have their own counter, separate from the plan allowance of the verification routes, so reading the balance can never be blocked by verification traffic and never consumes verification capacity.
The balance snapshot is cached for five seconds and the usage snapshot for thirty. Polling faster than that returns the same data. A sensible schedule is a minute for the balance and five minutes for usage. Dashboards that refresh on user action can call on demand.
Concurrency
The caches combine concurrent requests: if ten requests for the same organisation arrive together, one computation runs. Your own caching is still worth having, especially in a multi tenant application where one backend serves many staff members looking at the same page.
Failure behaviour
If StampMitra cannot read your data at that moment, the endpoints return a 503 with the code ACCOUNT_DATA_UNAVAILABLE. Optional parts of the balance, such as the last top-up or the estimates, fall back to null instead of failing the whole response. Treat a 503 as temporary and keep the last known good value in your monitor.
Security and privacy considerations
Treat the balance as commercially sensitive. Call the endpoints from a backend, never from a browser or a mobile app, because that would require shipping an API key to a client. The API security policy explains how keys should be stored and rotated.
Keep wallet headers out of anything customer facing. The state is harmless, but there is no reason to pass it to an end user's browser. Strip custom headers at your edge when you proxy responses.
Log the requestId of failures, and never log the key. The new endpoints return no personal data about your end users, only organisation level totals. The usage figures do reveal your business volume, so restrict who can read dashboards fed by them.
Principle of least privilege
Use a dedicated key for monitoring when your policy requires it. Give it a single verification scope, which is enough for the account endpoints, and store it separately from the key used by the application. If the monitoring key leaks, it can read your balance and nothing more costly than the single product call it is scoped to.
IP allowlists
If your keys are restricted to allowed IP addresses, the monitoring job must run from an allowed address as well. A 403 from an IP restriction looks different from the scope error INSUFFICIENT_SCOPE, and the message tells you which one you hit.
Testing in sandbox and going live
Start with a sandbox key. Call the balance endpoint and confirm that you receive the sandbox notice and zero amounts. Call a verification route and look at the rate limit headers. Run the code path that reads X-Wallet-State, but note that you will not see it in sandbox, so feed your handler a fake FINISHED value in a unit test to exercise the alert and the circuit.
A checklist before switching to a live key
- The scheduled balance check runs under a live key and alerts a real channel.
- The alert text names an owner and a backup.
- Deferrable jobs honour the paused flag when the state is
LOW_BALANCE. - Critical flows show a friendly message on
FINISHEDand do not retry a 402. - Your client treats every new header as optional and works when all of them are missing.
- Chart code uses the date strings of the daily series without timezone conversion.
- The monitoring job stays under the sixty requests a minute limit by a wide margin.
Dry run of a low balance
You cannot force a real wallet low on demand, and you should not spend money to test. Simulate: stub the balance endpoint in a staging environment to return LOW_BALANCE and then FINISHED, and watch your alerts, queues and messages behave. The sandbox and production keys guide explains the differences between environments you should keep in mind.
Troubleshooting
The wallet headers never appear. Check that you are using a live key. Sandbox keys never get them. Check that the call was authenticated and went through a route such as a verification route. On a freshly restarted server the first call on a route without metering may omit them. If none of your calls ever carry them, the headers may be switched off operationally; the balance endpoint still works.
The rate limit headers are missing on a route. Only routes that use the rate limiter send them. Compare with the table of routes above.
INSUFFICIENT_SCOPE on the balance endpoint. The key has no verification scope. Issue a key with at least one, or use an existing production key.
The balance looks stale. The snapshot can be five seconds old. After a recharge, wait a few seconds. If it stays wrong for minutes, send the requestId of your call to support.
Usage totals do not match my own counters. Usage counts calls recorded on the metered product routes with valid keys, by Indian day. Differences come from timezone cuts (your counters may use UTC), retries that you count once and we count twice, and calls stopped by limits that your code never sent a result for. The partition of billed, pending, failed and successful unbilled calls helps locate the gap.
spend is lower than the wallet movement. Pending debits are not in spend until they settle. Read again after a few minutes.
I get 429 on the account endpoints. Slow your polling to once a minute.
A 503 with ACCOUNT_DATA_UNAVAILABLE. A temporary failure. Retry after a short delay and keep the previous value.
What to include in a support request
Send the requestId from the error body, the approximate time in IST, the endpoint, and what you expected. Never send the full key; the first twelve characters of a key, the prefix, are enough to identify it.
How StampMitra Helps
StampMitra is a LegalTech platform run by Bani Global Industries LLP. It is not a government agency, and these endpoints do not change any legal or compliance obligation you have. They give your engineers and finance people the same facts the dashboard shows, in a form code can consume.
Today you can read your balance and usage with an API key, receive rate limit and wallet state hints on authenticated calls, import the Postman collection or the OpenAPI document to try the requests, and use the SDK pages for typed clients that pick up the two new operations. The verification docs list every product you can pay for from the wallet, and the PAN verification guide shows a complete first integration.
The honest limits of the feature as shipped: there is no per day request cap enforced on accounts today, so the per day limit in the usage response is null and its source says NONE; usage covers two months, not a longer history; the wallet headers are hints, not guarantees; and the balance is read, not changed, through these endpoints. Recharging happens in the dashboard or through the wallet endpoints.
We intend to keep this surface small and stable. Plans, which may change, include webhook style notifications when a wallet crosses a threshold so that polling becomes optional, and richer usage breakdowns by key. None of that is available today, and nothing in this guide depends on it. If you want one of those sooner, tell support, because requests from real integrations decide what we build next.
If you build the four layer pattern from this guide, you will find a low wallet the same hour it happens, not the morning after a customer complained. That is the whole purpose of exposing these numbers.
API reference at a glance
PAN
POST /v1/verify/panRequest parameters
| Field | Type | Description |
|---|---|---|
Authorizationrequired | header | Bearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted. |
panrequired | string | PAN, format ABCDE1234F. |
name | string | Name to match against records (max 256 chars). |
POST /v1/verify/pan{
"referenceId": "SMV-20260927-8F42K7",
"pan": "ABCDE1234F",
"type": "Individual",
"valid": true,
"registeredName": "RAHUL KUMAR",
"fatherName": "SURESH KUMAR",
"panStatus": "VALID",
"panStatusCode": "E",
"panStatusDescription": "Existing and valid PAN",
"nameMatchResult": "MATCH",
"nameMatchScore": "100",
"aadhaarSeedingStatus": "y",
"aadhaarSeedingStatusDescription": "Aadhaar linked",
"lastUpdatedAt": "2023-04-01",
"message": "PAN is Valid"
}POST /v1/verify/panPAN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).
Frequently asked questions
How do I check my StampMitra wallet balance over the API?
Send a GET request to /v1/account/balance with your API key. The response gives the balance in rupees and paise, the low-balance threshold, the state OK, LOW_BALANCE or FINISHED, an estimate of calls remaining and your last top-up. It is cached for five seconds.
Which API keys can read the balance and usage endpoints?
Any key that holds at least one verification scope, or an e-Stamp scope, can read its own organisation's balance and usage. No new scope exists. A key with only unrelated scopes receives a 403 with the code INSUFFICIENT_SCOPE, and a key can never read another organisation.
Is the exact balance available in a response header?
No. Live keys receive X-Wallet-State with OK, LOW_BALANCE or FINISHED, and X-Wallet-Balance-Low set to true only when the wallet is low. The amount is deliberately left out of headers because proxy and load balancer logs would otherwise record it.
What do the X-RateLimit headers mean?
X-RateLimit-Limit is the requests allowed in the current minute window, X-RateLimit-Remaining is what is left after this request, and X-RateLimit-Reset is the whole seconds until the window ends. A 429 also carries Retry-After with the same number of seconds.
Why can the wallet headers be missing from a response?
They come from server memory and are refreshed from balance data the metering step already reads, so StampMitra never delays your call to fetch them. Sandbox keys never get them, and they can be switched off operationally. Write clients that work without them.
How is a month defined in the usage endpoint?
Months and days use Indian Standard Time, UTC plus five thirty. A call at 00:00 IST on the first belongs to the new month, which is 18:30 UTC the evening before. The response includes the exact UTC instants and the timezone Asia/Kolkata.
Does the spend figure include GST?
No. Spend is the sum of billed call prices before GST, which is what moves the wallet. GST is charged when you recharge and appears on the invoice. Each month block also says gstIncluded false so nobody mistakes the figure for a tax inclusive amount.
What does a sandbox key see on these endpoints?
A sandbox key gets a sandbox notice, state OK and zero amounts from the balance endpoint, with no wallet read at all. The usage endpoint counts only sandbox calls with zero spend. Sandbox keys get twelve requests a minute on these endpoints, and rate limit headers only.
How should I alert on a low balance?
Run a scheduled balance check every few minutes, alert on state changes rather than every run, read X-Wallet-State on live calls for faster detection, pause deferrable jobs on LOW_BALANCE, queue critical work on FINISHED, and add a runway alert in days of average spend.
Sources
Verified 2026-10-30 against the StampMitra Developers codebase and the StampMitra B2B platform contract for the account API. No external websites were used for API facts in this guide.
- Account API contract for
GET /v1/account/balance,GET /v1/account/usageand the response headers (docs/ACCOUNT_API_CONTRACT.mdin the B2B platform repository), verified 2026-10-30. - Published OpenAPI document at /openapi.json, version 1.1.0, operations
getAccountBalanceandgetAccountUsage, verified 2026-10-30. - Authentication docs: key formats, scopes and rate limit notes, verified 2026-10-30.
- API errors and handling and sandbox versus production keys, verified 2026-10-30.
- Pricing page for plan limits and the product price list, verified 2026-10-30.