Most teams meet the e-Stamp API for the first time as a ticket that says "make the rental agreement journey end with a stamped PDF". The work behind that ticket is eight small steps, and the order matters: find out what your account may order, learn which fields a state wants, pick the right article, price the stamp, collect the parties, create the order, fund it, and then wait for a certificate that a human-reviewed, vendor-backed process produces. This page walks all eight steps against the real StampMitra B2B API at https://api-b2b.stampmitra.in, with the paths, scopes, request bodies and response fields as they exist today.
Two things make e-stamping different from the verification APIs on the same platform. First, nothing about it is instant: a successful response means your request was accepted, not that a stamp exists. Second, the most important number in the flow, the statutory duty, is not something StampMitra's API works out from your agreement. You decide the duty (or your compliance team does), and the API checks that the amount can actually be issued in that state and tells you what the whole order will cost.
StampMitra is a private LegalTech platform run by Bani Global Industries LLP. It is not a government body and not a law firm, and this page is general technical information, not legal advice. Duty, forms and registration differ by state and change over time, so confirm legal questions with qualified counsel or the state's stamp authority.
Key takeaways
- The flow has eight steps: catalogue, schema, state and article lookup, duty calculation, parties, order creation, funding, and status with certificate. Steps 1 to 3 are free lookups.
- Duty calculation and parties are metered calls. Order creation can carry the per-order API fee where it is enabled on your account. Statutory duty and the state procurement fee are passed through at actuals and are not part of the API fee.
POST /v1/estamp/ordersneeds anIdempotency-Keyheader. Retrying with the same key and body replays the original response; the same key with a different body returns 422IDEMPOTENCY_KEY_REUSED.- A new order lands in review, not at the vendor. Its first status is a pre-placement status such as
PENDING_REVIEW, and an operations reviewer confirms placement separately. - Money moves at order creation, from a Stamp Duty Wallet (duty, no GST) and a Service Fee Wallet (procurement and convenience fees, with GST collected at top-up). If either balance is short the order is refused and nothing is created.
- The certificate is fetched per stamp from
GET /v1/estamp/stamps/:stampId/certificate. Until it is delivered the call returnsavailable: falsewith HTTP 200, and the download link lives five minutes. - Sandbox keys never debit anything and run a simulated lifecycle: an order reaches
ISSUEDin about thirty seconds and the certificate is a watermarked sample, so you can test every step for free. The catalogue and prices are sample data, and the real review wait and real certificate need one production rehearsal.
What does the e-Stamp API do, and what does it leave to you?
An e-stamp is an electronic stamp certificate that replaces a physical stamp paper. It carries a unique certificate number that can be checked with the issuing authority, and it is issued against a specific state, a specific article of that state's stamp schedule and a specific duty amount. Not every state offers every article electronically. The state detail endpoint exposes this directly with two flags, eStampAvailable and traditionalAvailable, so your product can tell a user early that a combination is not available instead of failing at the end.
The StampMitra API covers the buying side of that process. Through one API key you can read the catalogue of states and articles, validate a stamp line against what the vendor will accept, price the order, create it, track it and download the certificate. It does not draft your agreement, decide which article applies, determine the duty, arrange execution or registration, or give legal advice. The platform's service terms are explicit that you remain responsible for choosing the correct document category and article and for reviewing the transaction details before submission, and that a technical amount displayed by an API does not override the statutory requirement for the underlying transaction.
That division of labour shapes the integration. Your product owns the legal question and the user relationship. The API owns the catalogue, the arithmetic on service charges and GST, the money movement and the delivery of the certificate. When you design screens, put the legal choices (document type, article, duty basis) in front of a human who can answer them, and let the API validate everything it can.
Who this is for
The people who end up integrating this are proptech teams stamping rent and leave and licence agreements, NBFCs and lenders stamping loan and security documents, marketplaces that sell legal documents, and HR or facility teams who stamp employment and vendor paperwork in volume. The same eight steps apply to all of them. What changes is the article you choose and the parties you collect. If your use case is rental agreements specifically, the rental agreement use case goes deeper on the product side, and there is a full rental example near the end of this page.
What are the eight steps and which call does each one use?
Think of the flow as three layers. The first layer is discovery and quoting: it reads data, costs little or nothing, and can run as often as your UI needs. The second is the order: it writes data, costs money and must be safe to retry. The third is delivery: it is asynchronous and you will poll or listen for it. The table lists every step with its endpoint, scope and whether the call is metered.
| Step | Purpose | Endpoint | Scope | Metered by StampMitra |
|---|---|---|---|---|
| 1 | Which e-Stamp services am I entitled to | GET /v1/services, GET /v1/services/:serviceCode | services.read | No |
| 2 | Which fields does e-Stamp need | GET /v1/estamp/schema/:serviceCode | estamp.read | No |
| 3 | States, articles, districts, SROs | GET /v1/estamp/states and the routes beneath it | estamp.read | No |
| 4 | Validate the amount and price the stamp | POST /v1/estamp/duty/calculate | estamp.duty.calculate | Yes |
| 5 | Attach or correct party details | POST /v1/estamp/orders/:orderId/parties | estamp.parties.write | Yes |
| 6 | Validate, then create the order | POST /v1/estamp/orders/validate, POST /v1/estamp/orders | estamp.orders.validate, estamp.orders.create | Create only, per order, where enabled |
| 7 | Fund the order | POST /v1/stamp-duty-wallet/topup, POST /v1/service-fee-wallet/topup, GET /v1/payments/intents/:id | stamp-duty-wallet.topup, service-fee-wallet.topup, payments.read | No |
| 8 | Track, then fetch the certificate | GET /v1/estamp/orders/:id, GET /v1/estamp/stamps/:stampId, GET /v1/estamp/stamps/:stampId/certificate | estamp.orders.read, estamp.stamps.read | No |
The product reference splits this into eight pages that line up with the steps: catalogue discovery, schema and options, state, instrument and article lookup, duty and value calculation, parties, order creation, payment and order status and document access. Each has its own long-form page with the details this overview skips.
One ordering note. The reference timeline lists parties before order creation, but the parties route takes an order id, so on the wire you create the order first and attach or correct parties afterwards. The stamp lines on the order already carry firstParty and secondParty, which is what normally ends up on the certificate. The parties route exists for the customer and counter-party contact details and for corrections.
What do I need before the first call?
You need an organisation with e-Stamp enabled, an API key with the right scopes, and a funded account if you are going to production. The pieces are small but each one has a way to fail that is easy to misread.
Entitlement. Every /v1/estamp/* route is guarded by an estamp.enabled entitlement. An organisation without it gets HTTP 400 on these routes with a message saying it is not entitled. Production access for e-Stamp is a separate approval from verification APIs, and all eight reference pages gate on one estamp service code, so a single approval covers the whole surface.
Keys and scopes. Keys are environment scoped: sk_test_ is sandbox and sk_live_ is production. They carry OAuth-style scopes, and sensitive scopes can be restricted to an IP allowlist. Authentication is Authorization: Bearer <key> or x-api-key: <key>. A missing or bad key is 401 with code UNAUTHORIZED, a valid key without the scope is 403 with code FORBIDDEN. Create keys per service: your quoting service needs services.read, estamp.read and estamp.duty.calculate; your order worker needs estamp.orders.validate, estamp.orders.create, estamp.parties.write and the wallet scopes; your status poller needs estamp.orders.read and estamp.stamps.read. The authentication reference lists how keys are created and rotated.
Errors. Every error, from every route, uses one envelope:
{
"error": {
"code": "BAD_REQUEST",
"message": "stampDutyAmount must be one of 100, 200, 500, 1000 for ART1 in MH",
"requestId": "a1b2c3d4-e5f6-4a1b-8c2d-9f0e1a2b3c4d"
}
}Log the requestId with every failure, because it is the first thing support will ask for. The message text is the useful part for the e-Stamp routes. The envelope's code is derived from the HTTP status (BAD_REQUEST, NOT_FOUND, CONFLICT, FORBIDDEN, PAYMENT_REQUIRED), so branch on the status first and treat the message as human-readable detail rather than a stable machine code.
Rate limits. Lookups allow 60 requests per 60 seconds per endpoint. Duty calculation, parties, validate, create and cancel allow 30 per 60 seconds. A 429 means back off; do not retry in a tight loop.
export STAMPMITRA_API_KEY="sk_test_xxxxxxxxxxxx" # sandbox key, keep it in your secret manager
export BASE="https://api-b2b.stampmitra.in"Step 1: how do I find out which e-Stamp services my account can order?
Start with the service catalogue. GET /v1/services returns every service the organisation is entitled to, verification and e-Stamp together, and GET /v1/services/:serviceCode returns one of them. e-Stamp services appear as normal entries with a serviceCode that begins with estamp..
curl -s "$BASE/v1/services" -H "Authorization: Bearer $STAMPMITRA_API_KEY"[
{
"serviceCode": "estamp.instrument.rent_agreement",
"name": "Rent Agreement e-Stamp",
"category": "estamp",
"basePrice": "500.00",
"currency": "INR",
"isActive": true,
"gstRatePercent": "18.00",
"defaultGovernmentFee": null,
"processingTimeHours": 24,
"guestCheckoutEnabled": false,
"requiresDocuments": true,
"uiSchema": null,
"entitled": true,
"entitlementUpdatedAt": "2026-09-01T10:00:00.000Z"
}
]Read entitled and isActive before you show a service in your UI, and treat processingTimeHours as an expectation to display, not a promise to enforce. basePrice on a catalogue entry is a generic service field; for e-Stamp the price of an actual order comes from the duty calculation in step 4, which prices duty and service charges per stamp. An empty list for e-Stamp means the organisation is not entitled yet. That is a configuration state, not an error, so do not retry; check your production access status in the dashboard or write to developer support.
Step 2: how do I learn which fields an e-stamp needs?
GET /v1/estamp/schema/:serviceCode returns the metadata to render a form. e-Stamp has exactly one schema, so the serviceCode in the path is accepted for route consistency and does not change the answer. The response is a list of field definitions with name, label, type, required and, where relevant, pattern and placeholder.
curl -s "$BASE/v1/estamp/schema/estamp.instrument.rent_agreement" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY"{
"fields": [
{ "name": "stateCode", "label": "State", "type": "text", "required": true, "placeholder": "MasterDataState.code, e.g. MH" },
{ "name": "articleCode", "label": "Article", "type": "text", "required": true, "placeholder": "MasterDataArticle.code, scoped to stateCode" },
{ "name": "districtCode", "label": "District", "type": "text", "required": false, "placeholder": "Required when the state requires SRO selection" },
{ "name": "sroValue", "label": "SRO", "type": "text", "required": false, "placeholder": "MasterDataSro.value, required when the state requires SRO selection" },
{ "name": "stampDutyAmount", "label": "Stamp duty amount", "type": "text", "required": true, "placeholder": "Government e-Stamp duty amount per the applicable State Stamp Act" }
]
}Notice what the schema describes: the inputs to the quote, which are the five fields of the duty calculation. It is the quote form, not the whole order form. Party names, purpose and the rest of the stamp line come from the order contract in step 6. The schema is small today and may grow, which is the reason to render from it rather than hardcode labels. Cache it, keep one real response as a test fixture, and let a failing fixture test tell you when it changes.
Step 3: how do I pick the state, the article and, where needed, the district and SRO?
Three routes under estamp.read give you the catalogue, and three more cover the places where a state wants extra detail.
| Route | Returns |
|---|---|
GET /v1/estamp/states | Active e-Stamp states |
GET /v1/estamp/states/:stateCode | One state: name, eStampAvailable, traditionalAvailable, minAmount, maxAmount, sroRequired, cessRequired, propertyAreaRequired, fatherHusbandNameRequired, firstPartyAddressRequired, panCardImageRequired, instrumentCount |
GET /v1/estamp/states/:stateCode/instruments | The state's article catalogue |
GET /v1/estamp/instruments/:instrumentCode/articles | The same catalogue; the path parameter is a state code |
GET /v1/estamp/states/:stateCode/requirements | Documentary and data requirements for the state (404 until synced) |
GET /v1/estamp/states/:stateCode/districts and .../districts/:districtCode/sros | Districts and sub-registrar offices |
Two quirks are worth knowing before you design a cascading select. There is no separate instrument entity: the instruments and articles routes return the same state-scoped list, so your UI has two levels, state then article, not three. And the articles route takes a state code in its instrumentCode slot. Article entries carry a code, a description, a popular flag, a minimumAmount, an optional allowedAmounts list and an isSecondPartyNA flag that tells you the second party is not applicable for that article.
curl -s "$BASE/v1/estamp/states/MH" -H "Authorization: Bearer $STAMPMITRA_API_KEY"{
"code": "MH",
"name": "Maharashtra",
"isActive": true,
"eStampAvailable": true,
"traditionalAvailable": true,
"minAmount": "100.00",
"maxAmount": "500000.00",
"sroRequired": true,
"cessRequired": false,
"propertyAreaRequired": true,
"fatherHusbandNameRequired": false,
"firstPartyAddressRequired": true,
"panCardImageRequired": false,
"instrumentCount": 42
}The numbers in that response are illustrative, not a statement about any state's rules. The point is the shape: the flags tell you which extra inputs to collect. If sroRequired is true you must send districtCode and sroValue to the duty calculation and the order, and you read them from the districts and SROs routes. If propertyAreaRequired is true the stamp line needs propertyArea. Drive your form from these flags, not from a per-state if in your code. When the master data changes, the flags change with it.
Step 4: what does the duty calculation actually calculate?
This is the step people misread, so here is the exact behaviour. POST /v1/estamp/duty/calculate takes a state, an article and a stampDutyAmount that you supply. It does not derive that amount from the rent, deposit or property value. What it does is check that the amount is a valid purchase for that state and article, and then price everything around it: service charges, GST on those charges, and any coupon. The platform's terms describe the same split: StampMitra does not determine statutory duty unless a service expressly offers a calculation based on authorised data.
What the validation covers matters because it explains most 400 responses. A state or article may allow only fixed denominations, a range between a minimum and maximum, or in some configurations a total built from a limited number of papers. A value outside the allowed set is rejected with a message that lists what is allowed. If a duty rule is configured for the state, the response carries a requiredDuty figure: the minimum the rule demands for the selection, which you may exceed by buying a larger denomination but not undercut.
curl -s -X POST "$BASE/v1/estamp/duty/calculate" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "stateCode": "MH", "articleCode": "ART1", "districtCode": "PUNE", "sroValue": "SRO-14", "stampDutyAmount": 1000 }'The response is a breakdown. The fields below are the ones the pricing engine produces; every amount in the example is made up for illustration.
{
"stateCode": "MH",
"articleCode": "ART1",
"districtCode": "PUNE",
"sroValue": "SRO-14",
"fulfilmentSpeed": "STANDARD",
"papers": [{ "value": 1000, "count": 1 }],
"paperCount": 1,
"requiredDuty": null,
"stampDutyAmount": 1000,
"serviceCharges": [
{ "key": "PROCUREMENT_FEE", "basis": "FIXED", "amount": 100, "couponEligible": true, "unitAmount": 100, "quantity": 1 }
],
"serviceChargeSubtotal": 100,
"discountAmount": 0,
"taxableServiceCharges": 100,
"gstAmount": 18,
"gstRatePercent": 18,
"totalAmount": 1118,
"masterDataVersion": 14,
"masterDataVersionStale": false,
"pricingVersion": 7
}Read the structure rather than the numbers. Statutory duty (stampDutyAmount) is never taxed and never discounted. GST applies to the service charges only. totalAmount is the figure to show a customer. Display what comes back, do not recompute it, and do not keep a duty table in your own database. If you pass masterDataVersion, the response tells you via masterDataVersionStale whether your cached catalogue is out of date. The calculation is metered, so cache a quote against a hash of its inputs instead of calling on every keystroke. The state duty explainer covers how to decide the duty amount in the first place, and the duty calculation reference page covers every field and error.
Step 5: how do parties and witnesses fit in?
An e-stamp is issued in the names of the parties to the instrument. On the order, each stamp line carries firstParty (required) and secondParty (where the article uses one), plus optional stampDutyPayer, payerPhone and payerGender. After the order exists, POST /v1/estamp/orders/:orderId/parties attaches or corrects the contact details held for the order, with these optional fields: customerName, customerMobile, customerEmail, partyAddress and partyPan. The route models a single party: partyName is accepted but writes the same name as customerName, so it does not set the counter-party. The counter-party's name travels on the stamp line as secondParty.
curl -s -X POST "$BASE/v1/estamp/orders/<orderId>/parties" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "customerName": "Anita Rao", "customerMobile": "+919800000001", "customerEmail": "[email protected]", "partyAddress": "Flat 4B, Example Heights, Pune" }'The orderId here is the Core order id. The create response carries both an id and an orderId, and this route takes the orderId field. Check that in your first sandbox response rather than assuming. The call is metered and meant for the review stage, when a reviewer can still act on a correction. After issuance, an e-stamp cannot generally be edited, and the service terms say a correction may need a new transaction, which is why your UI should include a review screen before you create the order.
Witnesses. The API has no witness fields, and that is correct, not an omission. Witnesses sign the agreement, not the stamp purchase. Whether an agreement needs witnesses, how many, and whether they must be present at execution are questions for the document and the state's rules. Keep witness details in your agreement workflow. If your signing flow uses e-sign or a physical signature, the e-stamp certificate is simply one of the inputs to that step.
Step 6: how do I create the order without creating two?
Validate first, then create. POST /v1/estamp/orders/validate runs the same validation as creation without saving anything, and returns HTTP 200 whether the result is READY_TO_PLACE, has WARNINGS or NEEDS_INFORMATION. Only unfixable input returns 400 with VALIDATION_FAILED. Use it to show field-level problems and pricing warnings to your user before they commit. Validate is not charged.
The body for both routes is a reference of your own and a stamps array. Every line shares one state, because one order is one state. A line needs state, articleCode, denomination and firstParty; category defaults to e_stamp; the rest is optional detail such as secondParty, purpose, district, sro, propertyArea, cessAmount, quantity, clientReference, costCentre and businessUnit.
curl -s -X POST "$BASE/v1/estamp/orders" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rent-lease-2026-0412-v1" \
-d '{
"reference": "lease-2026-0412",
"stamps": [{
"clientReference": "line-001",
"state": "MH",
"category": "e_stamp",
"articleCode": "ART1",
"denomination": 1000,
"firstParty": "Vikram Shetty",
"secondParty": "Anita Rao",
"purpose": "Leave and licence agreement",
"district": "PUNE",
"sro": "SRO-14",
"propertyArea": "850 sq ft"
}]
}'One caution about party fields before you copy that body. The public order contract takes firstParty and secondParty as plain names, and propertyArea as a string. The issuing side models a party as an object with name, address, phone, fatherHusbandName and gender, and a property area as a unit and a value, which is where the state flags for father or husband name and first-party address point. Run your first sample through POST /v1/estamp/orders/validate in sandbox and keep the accepted shape as a test fixture, because this is the part of the contract most likely to be refined.
The Idempotency-Key header is mandatory on this route and only this route. Retrying with the same key and the same body replays the original response and creates no second order. The same key with a different body is rejected with 422 IDEMPOTENCY_KEY_REUSED. A missing header is a 400 IDEMPOTENCY_KEY_REQUIRED. A second request that arrives while the first is still being processed gets 409 IDEMPOTENCY_IN_PROGRESS with a Retry-After header: wait, then resend the identical request. Keys are scoped to your organisation, the route and the environment, and are kept for 24 hours. Derive the key from something stable in your system, such as your agreement id plus a version, so a retry after a timeout reuses it and a genuinely new order does not.
{
"id": "estord_9c1f2a3b",
"orderId": "6a0e2b9a-5f1d-4c63-9c2e-0d4e7b1f3a55",
"reference": "lease-2026-0412",
"state": "MH",
"status": "PENDING_REVIEW",
"isTest": false,
"stampCount": 1,
"stamps": [{ "id": "eststamp_4f21ab", "clientReference": "line-001" }],
"errors": [],
"warnings": [],
"createdAt": "2026-10-29T09:12:03.000Z"
}The status is Core's pre-placement review status: PENDING_REVIEW, READY_TO_PLACE, WARNINGS, NEEDS_INFORMATION, VALIDATION_FAILED or CANCELLED. These are deliberately not the vendor's own vocabulary. Order creation stops at validation. It does not place the order with the issuing vendor; a human operations reviewer confirms placement separately, so a new order will sit in review for a while. The product reference also lists the platform-wide POST /v1/orders for any entitled service code. It takes a free-form details object and has no typed state, article or denomination fields, so for stamping the e-Stamp routes above are the better fit.
Step 7: how is the stamp paid for?
Three balances exist on a StampMitra account, and keeping them apart in your head saves a lot of confusion.
| Balance | Pays for | GST | How it is funded |
|---|---|---|---|
| API credit wallet | StampMitra's per-call API fee on metered calls | Billed with GST invoice | Dashboard top-up, minimum top-up applies |
| Service Fee Wallet | Procurement and convenience fees on e-Stamp orders | Added on top at top-up; the order debits the amount net of GST | POST /v1/service-fee-wallet/topup, invoices at GET /v1/service-fee-wallet/invoices |
| Stamp Duty Wallet | The statutory duty itself | None, ever | POST /v1/stamp-duty-wallet/topup |
Both e-Stamp wallets are topped up through Razorpay Checkout: the top-up call takes a baseAmount in rupees and returns a razorpayOrderId and razorpayKeyId your frontend uses to open checkout. For the Service Fee Wallet the response also shows the gstAmount added and the totalCharged. Balances and ledgers are readable with GET /v1/stamp-duty-wallet, GET /v1/service-fee-wallet and their /transactions routes.
The key behaviour is when money moves. At order creation, before any order record is written, the platform debits the duty from the Stamp Duty Wallet and the service fee from the Service Fee Wallet. If the second debit fails, the first is reversed. If either balance is short, the request is refused and nothing is created. The answer is a 402 PAYMENT_REQUIRED whose message names the wallet that is short, the Stamp Duty Wallet or the Service Fee Wallet, so you know which one to top up. After you top up, retry with the same Idempotency-Key and the call runs cleanly instead of replaying a cached failure. If order creation itself fails after the debits, they are credited back. Pre-check balances with the wallet routes before you let a user press the final button.
The reference page for payment also documents the generic payment resource: POST /v1/payments/intents creates a Razorpay-backed intent (fields externalOrderId, amountMinor, currency and paymentConnectionId) and GET /v1/payments/intents/:id reads it. Treat that as the generic building block, and the wallets as the path e-Stamp orders actually draw from. Whatever the screen says, a successful payment is not an issued stamp. Move your own record to something like "paid, awaiting issuance" and only to "stamped" once a certificate is available.
Step 8: how do I track the order and get the certificate?
Tracking has two levels because an order is a container for stamps. GET /v1/estamp/orders/:id returns the order and its pre-placement status, and GET /v1/estamp/orders lists your organisation's orders with page and limit. Once a reviewer has placed the order, each stamp has its own lifecycle: GET /v1/estamp/stamps/:stampId returns the stamp with a status of PLACED, ASSIGNED, GENERATING, VERIFYING, VERIFIED or CANCELLED, a verified boolean, and once generated a uin, the unique identification number of the stamp.
curl -s "$BASE/v1/estamp/stamps/eststamp_4f21ab/certificate" \
-H "Authorization: Bearer $STAMPMITRA_API_KEY"The certificate route is a deliberate passthrough with an explicit discriminator, so it always returns HTTP 200:
{ "available": false, "status": "not_yet_available" }and once the certificate has been delivered:
{
"available": true,
"status": "available",
"downloadUrl": "https://api-b2b.stampmitra.in/v1/estamp/certificates/eststamp_4f21ab/download?token=...",
"expiresAt": "2026-10-29T10:05:00.000Z",
"certificateNumber": "IN-MH-2026-0917-001234"
}The downloadUrl is always hosted by StampMitra, never by a third party, and it expires after five minutes. Fetch it at the moment you need it, stream the PDF to your own storage and keep it there, because you cannot rely on the link later. certificateNumber can be null even when the certificate is available. A stamp that has not been delivered yet is not an error, so poll with backoff instead of treating available: false as a failure.
// Node 18+, global fetch
const BASE = "https://api-b2b.stampmitra.in";
const headers = { Authorization: `Bearer ${process.env.STAMPMITRA_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function waitForCertificate(stampId, { maxMs = 60 * 60_000 } = {}) {
const started = Date.now();
let delay = 30_000;
while (Date.now() - started < maxMs) {
const res = await fetch(`${BASE}/v1/estamp/stamps/${stampId}/certificate`, { headers });
if (res.status === 429) { await sleep(delay * 2); continue; }
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error?.message), { status: res.status, requestId: body.error?.requestId });
if (body.available) return body;
await sleep(delay);
delay = Math.min(Math.round(delay * 1.5), 5 * 60_000);
}
return null; // hand the stamp to a manual review queue
}import os, time, requests
BASE = "https://api-b2b.stampmitra.in"
H = {"Authorization": f"Bearer {os.environ['STAMPMITRA_API_KEY']}"}
def fetch_certificate(stamp_id: str, out_path: str, max_s: int = 3600):
deadline, delay = time.time() + max_s, 30.0
while time.time() < deadline:
r = requests.get(f"{BASE}/v1/estamp/stamps/{stamp_id}/certificate", headers=H, timeout=20)
if r.status_code == 429:
time.sleep(delay * 2); continue
r.raise_for_status()
body = r.json()
if body["available"]:
pdf = requests.get(body["downloadUrl"], timeout=60) # link lives five minutes
pdf.raise_for_status()
with open(out_path, "wb") as f:
f.write(pdf.content)
return body.get("certificateNumber")
time.sleep(delay); delay = min(delay * 1.5, 300)
return NoneA stamp can also be cancelled with POST /v1/estamp/stamps/:stampId/cancel while it is in a cancellable vendor state, replaced with POST /v1/estamp/stamps/:stampId/replace after a generation defect, and an order can be cancelled with POST /v1/estamp/orders/:id/cancel before a reviewer has placed it. Each returns 409 INVALID_STATE if the object has moved past the point where the action is possible. A verified stamp cannot be cancelled.
How do order and stamp states fit together?
There are two vocabularies and they describe different moments. The order status describes review, before anything exists at the vendor. The stamp status describes the vendor's work, after placement. Your database should store both and never merge them into one enum.
| Where | Value | Meaning |
|---|---|---|
| Order | PENDING_REVIEW | Created, waiting for review |
| Order | READY_TO_PLACE | Validation passed, ready for a reviewer to place |
| Order | WARNINGS | Passed with warnings, usually pricing or compliance notes |
| Order | NEEDS_INFORMATION | A reviewer needs more detail |
| Order | VALIDATION_FAILED | Input could not be validated |
| Order | CANCELLED | Cancelled before placement |
| Stamp | PLACED | Placed with the issuing vendor |
| Stamp | ASSIGNED | A stamp has been assigned to the line |
| Stamp | GENERATING | The certificate is being generated |
| Stamp | VERIFYING | Generated, being verified |
| Stamp | VERIFIED | Verified; the certificate follows |
| Stamp | CANCELLED | Cancelled at the vendor |
Map any string you do not recognise to a "needs attention" bucket rather than throwing. The vocabulary is a contract you read from responses, and extending it should never crash your poller.
Webhooks or polling: what should I rely on?
The webhooks reference lists order lifecycle events: order.created, order.payment_confirmed, order.fulfilment_started, order.fulfilment_completed, order.failed, order.cancelled, order.refund_completed and order.document_ready. Deliveries are signed: the signature is a hex HMAC-SHA256 of the timestamp, a dot and the raw body, sent in X-Webhook-Signature with X-Webhook-Timestamp and X-Webhook-Event-Id headers. During a secret rotation an X-Webhook-Signatures header carries every currently valid signature. Failed deliveries are retried with exponential backoff, six attempts over roughly thirteen hours, then dead-lettered. Verify against the raw bytes, reject timestamps far from your clock (the SDKs default to 300 seconds), de-duplicate on the event id and answer 2xx fast. The webhooks page has verification code in seven languages.
Register your endpoint from the Webhooks page of the dashboard. Be clear about what the events describe: they are keyed to the platform's generic order lifecycle. For an e-Stamp order created through the dedicated routes, do not assume every transition above fires, and confirm event coverage for your account with developer support. The safe design is the one that works either way: treat an event as a hint to look, then fetch the stamp and certificate routes and act on what they say, and keep a slow poll as a backstop for anything that stays pending.
What goes wrong, and which failures are safe to retry?
Most failures in this flow are predictable. The table separates the ones where a retry helps from the ones where it does harm.
| Situation | Signal | What to do |
|---|---|---|
| Key missing or wrong | 401 UNAUTHORIZED | Fix the key. Do not retry |
| Key lacks the scope | 403 FORBIDDEN | Add the scope, or use the right key |
| Not entitled to e-Stamp | 400 | Check production access status, contact support |
| Amount not issuable | 400 with a message listing allowed values | Show the allowed denominations, let the user choose |
| State or article not available | 400 or 404 | Refresh the catalogue, offer another article |
Missing Idempotency-Key | 400 IDEMPOTENCY_KEY_REQUIRED | Add the header and retry |
| Same key, different body | 422 IDEMPOTENCY_KEY_REUSED | A bug on your side: new key for a new order |
| Same key while the first request is still running | 409 IDEMPOTENCY_IN_PROGRESS with Retry-After | Wait, then resend the identical request |
| Order not in a cancellable state | 409 INVALID_STATE | Stop; read the current status |
| e-Stamp temporarily paused | 409 SERVICE_UNAVAILABLE with a message | Retry later with backoff, tell the user |
| API fee cannot be covered | 402 PAYMENT_REQUIRED | Top up the API credit wallet, then retry |
| Duty or fee balance too low | 402 PAYMENT_REQUIRED, and the message says which wallet is short. Refused before anything is created | Top up that wallet, retry with the same Idempotency-Key |
| Rate limited | 429 | Back off exponentially |
| Upstream unavailable | 502 INTERNAL_ERROR | Retry with backoff, same Idempotency-Key |
Pricing rules help here. Malformed requests rejected up front with HTTP 400 are not charged, and calls that fail on StampMitra's side or its provider's side are credited back automatically. Duplicate protection is why the idempotency key is cheap insurance: a retry after a network timeout, when you do not know whether the order was created, replays instead of acting twice.
What does one e-stamp actually cost to run?
Break the cost into the parts that behave differently. The StampMitra API fee is ₹5 per charged call on pay-as-you-go, lower on Growth and Scale, and you will find the exact rate for your plan on the pricing page. Charged calls are duty calculation and parties, and order creation where the per-order fee is enabled on your account. Lookups and validation are free. The state procurement fee depends on state and quantity and is shared once production access is approved. The statutory duty is passed through at cost and never marked up. GST at 18 percent applies to StampMitra's own charges, not to the duty. Prices on the site exclude GST.
So a single rental stamp costs: the duty you decided, plus the procurement and convenience fees for the state with GST on them, plus the API fee for each charged call you made on the way. Every quote you request before the order adds to the API fee, which is why caching quotes against their inputs and calling parties once are worth the effort. The wallet balances have minimum top-ups before GST, and the API credit wallet alerts at low balance. Your request logs show each charged call; retention runs from 7 to 365 days depending on plan.
How is sandbox different from production?
Sandbox runs a simulated lifecycle, so you can test the whole flow for free. A sk_test_ key never debits a real wallet and never reaches the issuing side. Its catalogue is a small sample and its fee schedule is illustrative, so a sandbox quote has the production structure with different numbers. Orders move through CREATED, PAYMENT_PENDING, PROCESSING and ISSUED by themselves in about thirty seconds, stamps become VERIFIED, and the certificate route returns a short-lived link to one shared PDF watermarked SANDBOX SAMPLE, NOT A VALID STAMP. Two sandbox wallets hold fake credit that you can top up, orders debit them and failed orders credit them back. Special reference values such as SBX-PAYMENT-FAILED and SBX-ISSUANCE-FAILED force the failure paths, and a sandbox webhook endpoint receives the order.* events. Every sandbox request counts toward the sandbox limits, whichever e-Stamp or wallet route it is, catalogue reads and wallet calls included: 10 requests a minute and 200 a day per organisation, and 50 a day per IP address, resetting at midnight IST.
What stays different is what a person or the real world decides. A production order waits for a human reviewer and starts in the pre-placement statuses, so the timing and statuses you see in sandbox are not a promise. The certificate is a sample and not a stamp. The live catalogue, state availability and prices are production facts.
Plan your testing in two passes. Run steps 1 to 8 completely in sandbox, including every failure marker, the open-order limit, the 402 from a short wallet and your webhook handler. The order page has the full list of lifecycle statuses and test inputs, and the document page covers the sample certificate. Then, before launch, rehearse one real low-value production order so you have seen the real review wait and the real certificate once. Production needs an approved production access request and funded wallets. The sandbox versus production guide covers key handling and go-live in more depth.
Worked example: stamping a leave and licence agreement end to end
Here is the whole flow for one agreement, with every call. The parties are invented. Anita Rao is licensee, Vikram Shetty is licensor, the property is in Pune, and the compliance team has decided that the duty for this agreement is 1,000 rupees, which is an assumption for the example, not a statement of what any state charges. Registration and execution happen outside the API.
1. Entitlement. Your service calls GET /v1/services and finds estamp.instrument.rent_agreement with entitled: true.
2. Schema. It fetches GET /v1/estamp/schema/estamp.instrument.rent_agreement and builds the quote form from the five fields.
3. Lookup. The user selects Maharashtra. GET /v1/estamp/states/MH shows eStampAvailable: true and sroRequired: true, so the form asks for a district and an SRO, loaded from GET /v1/estamp/states/MH/districts and the SRO route beneath it. GET /v1/estamp/states/MH/instruments fills the article list, and the user (or your compliance rule) picks the article for a leave and licence agreement.
4. Quote. The service posts the five fields with stampDutyAmount: 1000. A 400 listing allowed denominations sends the user back to choose a valid amount. A 200 returns the breakdown, and the screen shows totalAmount, with a note of what comprises duty and what comprises fees. The quote is cached against its inputs.
5. Validate. Just before the final screen, the service posts the stamp line to POST /v1/estamp/orders/validate. A result with WARNINGS is shown to the user to read. VALIDATION_FAILED goes back to the form.
6. Wallet check. The service reads GET /v1/stamp-duty-wallet and GET /v1/service-fee-wallet and, if either balance is below the quote, prompts a top-up via the top-up routes before offering the button.
7. Create. The service posts the order with Idempotency-Key: rent-lease-2026-0412-v1 and stores the returned id, orderId and stamps[0].id alongside its own agreement id. The agreement's state in your database becomes stamp_requested.
8. Parties. If the review stage needs a correction, the service posts to /v1/estamp/orders/<orderId>/parties with the contact details.
9. Track. A scheduled job reads GET /v1/estamp/orders/estord_9c1f2a3b and, once the status moves past review and the stamp is placed, GET /v1/estamp/stamps/eststamp_4f21ab. It records each status change with a timestamp. The agreement's state becomes stamp_in_progress.
10. Certificate. When GET .../certificate returns available: true, the job downloads the PDF within five minutes, stores it, records the certificateNumber and uin, and sets the state to stamped. The user is notified, and the agreement can go to signing and, where the state requires it, registration.
Everything the user sees along the way is derived from API responses. Nothing is calculated locally, nothing is marked complete from a payment alone, and every write can be retried safely.
What should I check before going live?
Run this list once, with someone who owns the legal side in the room.
- Production access is approved for e-Stamp and the production key has only the scopes each service uses.
- Both e-Stamp wallets and the API credit wallet are funded, and you alert on low balances.
- Every order creation sends a stable
Idempotency-Keyderived from your agreement id. - Your code handles every order and stamp status, including unknown ones.
- Certificates are copied to your storage the moment they are available, not linked from the response.
- Your database has no duty table and no local arithmetic on amounts returned by the API.
- Your UI shows the article and duty basis to a human before submission, with a review screen before create.
- Webhook signatures are verified against raw bytes, and a slow poll catches anything pending too long.
- You have run one real low-value order through production, end to end.
How StampMitra helps
StampMitra gives developers one API key and one set of conventions across e-Stamp and the verification products: the same error envelope, the same scope model, the same request log and the same prepaid billing. The e-Stamp surface described here is what exists today, and this page has tried to be exact about the places where behaviour is asymmetric, such as review before placement, the five-minute certificate link and the absence of simulated issuance.
If you also verify the people on the agreement, the PAN verification API and GSTIN verification API use the same key. For the deeper developer walk-through of order mechanics see the e-Stamp order flow guide, and for rental product design see the rental agreement use case. To try the lookups, get a sandbox key and call the first three steps today. The sandbox is free and the production request is made from your dashboard when you are ready.
API reference at a glance
e-Stamp Catalog Discovery
GET /v1/servicesRequest 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. |
GET /v1/services[
{
"serviceCode": "estamp.instrument.rent_agreement",
"name": "Rent Agreement e-Stamp",
"category": "estamp",
"basePrice": "500.00",
"currency": "INR",
"isActive": true,
"gstRatePercent": "18.00",
"defaultGovernmentFee": null,
"processingTimeHours": 24,
"guestCheckoutEnabled": false,
"requiresDocuments": true,
"uiSchema": null,
"entitled": true,
"entitlementUpdatedAt": "2026-09-01T10:00:00.000Z"
}
]GET /v1/servicese-Stamp Catalog Discovery: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).
Duty / Value Calculation
POST /v1/estamp/duty/calculateRequest 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. |
stateCoderequired | string | State code, e.g. MH. |
articleCoderequired | string | Stamp article code from the articles lookup. |
stampDutyAmountrequired | number | Stamp duty amount to calculate charges for; must be positive. |
districtCode | string | District code, where the state needs one. |
sroValue | string | Sub-registrar office value, where the state needs one. |
doorstepDelivery | boolean | Whether doorstep delivery is requested. |
couponCode | string | Coupon code to apply. |
masterDataVersion | integer | Master-data version the caller last read. |
POST /v1/estamp/duty/calculateDuty / Value Calculation: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).
e-Stamp Order Creation
POST /v1/ordersRequest 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. |
Idempotency-Key | header | Optional idempotency key (1 to 255 characters), kept for 24 hours. The same key and the same request replays the stored response; the same key with a different request is 422 IDEMPOTENCY_KEY_REUSED; the same key while the first request is still running is 409 IDEMPOTENCY_IN_PROGRESS (see Retry-After). |
externalOrderIdrequired | string | Your own reference for this order. |
externalCustomerIdrequired | string | Your own reference for the customer. |
serviceCoderequired | string | The entitled service to order (e.g. an estamp.* code). |
details | object | Free-form service-specific payload (e-Stamp field shape is not finalized). |
POST /v1/orders{
"externalOrderId": "ord_ext_123",
"createdAt": "2026-10-15T09:41:07.000Z",
"serviceCode": "estamp.instrument.rent_agreement"
}POST /v1/orderse-Stamp Order Creation: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).
Order Status & Document Access
GET /v1/orders/:id/documentsRequest 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. |
GET /v1/orders/:id/documentsOrder Status & Document Access: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).
Frequently asked questions
What is an e-Stamp API?
An e-Stamp API lets software buy electronic stamp certificates programmatically: look up states and articles, price a stamp, create an order, pay and download the certificate, instead of a person using a stamping portal or a vendor counter.
Does the API work out the stamp duty for my agreement?
No. You supply the duty amount in stampDutyAmount. The API validates that the amount can be issued for the chosen state and article, then prices service charges and GST around it. Deciding the duty is a legal question for you or your counsel.
Why does my new order show PENDING_REVIEW instead of a certificate?
Order creation stops at validation and does not place the order with the issuing vendor. A human operations reviewer confirms placement separately, so a new order starts in a pre-placement status and the certificate follows later.
Is the Idempotency-Key header really mandatory?
Yes, on POST /v1/estamp/orders only. A missing header returns 400 IDEMPOTENCY_KEY_REQUIRED. Retrying with the same key and body replays the original response, and the same key with a different body returns 422 IDEMPOTENCY_KEY_REUSED, so a timeout can never create two paid orders.
Which wallets pay for an e-stamp order?
At order creation the platform debits the statutory duty from your Stamp Duty Wallet (no GST) and the procurement and convenience fees from your Service Fee Wallet. The API credit wallet separately pays StampMitra's per-call API fee.
What does the e-Stamp API fee cost?
On pay-as-you-go the API fee is ₹5 per charged call, lower on Growth and Scale, excluding 18 percent GST. Duty and the state procurement fee are quoted per order and passed through at actuals, so they are not part of that fee.
How do I download the e-stamp certificate?
Call GET /v1/estamp/stamps/:stampId/certificate. While it returns available false, keep polling. When available is true it returns a StampMitra-hosted downloadUrl that expires in five minutes, so stream the PDF into your own storage immediately.
Can I test the full e-stamp flow in sandbox?
Yes. A sandbox key runs a simulated lifecycle: orders reach ISSUED in about thirty seconds, the certificate route returns a watermarked sample PDF, and fake wallets can be topped up. Amounts and the catalogue are sample data, so rehearse one real production order before launch.
Does the API handle witnesses?
No, and it should not. Witnesses sign the agreement, not the stamp purchase. The stamp line carries first party and second party names, and the parties route carries contact details. Keep witness data in your agreement workflow.
What happens if I retry a failed order after topping up a wallet?
Reuse the same Idempotency-Key. If the earlier attempt was refused for a short balance, nothing was created and the debits were reversed, so the retry runs cleanly after your top-up instead of replaying a cached failure.
Which states and articles are supported?
Read them from GET /v1/estamp/states and the instruments route rather than hardcoding. Each state reports eStampAvailable and traditionalAvailable flags, and availability varies by state and article, so your product should check before collecting details.
Does the API register the agreement?
No. The API buys and delivers the e-stamp. Drafting, execution, e-sign and registration are outside it, and registration rules depend on the state and the document, so confirm them with qualified counsel.
Sources
- StampMitra e-Stamp API reference, docs/estamp, checked 2026-10-29.
- StampMitra webhooks reference, webhooks, checked 2026-10-29.
- StampMitra Verification, e-Stamp and e-Sign Service Terms, sections 11 to 14, checked 2026-10-29.
- StampMitra pricing and billing rules, checked 2026-10-29.