StampMitraDevelopers

Use cases

E-Stamping Rental Agreements via API for Proptech and Broker Apps

To e-stamp a rental agreement via API, resolve the state and article with the lookup endpoints, call the duty calculation endpoint, create an order with an e-Stamp serviceCode, attach the parties, collect payment, then poll status and download the certificate. Registration rules differ by state, so confirm them with qualified counsel.

By StampMitra Developer TeamLast updated: Read this in Hindi

E-stamping a rental agreement via API means your proptech or broker app drives the whole stamp-paper step programmatically. You look up the state and instrument, ask the API to calculate duty for the deal, create an order, attach the landlord and tenant, collect payment, and then download the issued e-Stamp certificate. With the StampMitra e-Stamp APIs those steps map to a small set of documented endpoints under /v1/estamp, /v1/orders, /v1/payments and /v1/orders/:id/documents, all authenticated with a Bearer API key.

Your code never computes the duty itself. Duty and service charges come back from POST /v1/estamp/duty/calculate, and the response amount is not recalculated or overridden client-side. The rest of the work on your side is product engineering: collecting clean party data, remembering which state and article the user picked, making retries safe, and storing the issued document where the landlord, tenant and broker can find it later.

E-stamping is only one part of making a tenancy enforceable. Whether a rental or leave-and-licence agreement must also be registered, and by whom, differs by state and by term. This guide explains the engineering. It is general information, not legal advice, and you should confirm registration rules with qualified counsel.

Key takeaways

  • The flow is lookup, calculate, order, parties, payment, document. Each step has its own endpoint and scope, so give your API key only the scopes your app needs.
  • Duty is always calculated by the API. Never hardcode a duty figure, a percentage or a state table in your app. Both the amount and the rules can change.
  • A rental agreement in this API is an order created through the generic POST /v1/orders with an e-Stamp serviceCode (for example estamp.instrument.rent_agreement in the docs example) and a free-form details object.
  • The details shape for e-Stamp is documented as not finalized, so treat it as data you build from the schema endpoint, keep it behind one adapter in your code, and re-check the docs before each release.
  • Use your own externalOrderId and externalCustomerId on every order, and send an Idempotency-Key on order creation and payment intents so a retry does not create a second order.
  • Webhooks are marked coming soon on the public webhooks page, so today the dependable pattern is to poll GET /v1/orders/:id/status with backoff.
  • Store the issued certificate in your own storage after download. Download URLs are typically short-lived and signed, so never save the URL as the record.
  • E-stamping does not by itself settle registration. Rules differ by state, so check with qualified counsel and the state's own registration department.

What does an e-stamping flow look like for a rental app?

Picture a generic broker app called HomeLeaf. A landlord and a tenant agree on terms inside the app, and HomeLeaf generates a draft agreement. Before the parties sign, the draft has to be stamped. Previously that meant someone going to a vendor or a treasury counter. With an API, HomeLeaf's backend does it in a few calls while the users wait on a progress screen.

The sequence has six moving parts. First, discover what your organisation is entitled to with GET /v1/services. Second, resolve the state and instrument through the lookup endpoints. Third, calculate duty. Fourth, create an order and attach parties. Fifth, create a payment intent and wait for it to confirm. Sixth, fetch the order's documents and store them.

Who does what

Your application owns the user experience, the draft agreement text, the consent screens and the storage of final documents. StampMitra, a private LegalTech platform operated by Bani Global Industries LLP, exposes the e-Stamp endpoints and meters your calls against your fulfilment wallet. StampMitra is not a government agency and not a law firm, so it does not give legal advice about your agreement or tell you whether a given tenancy needs registration.

The endpoints at a glance

StepMethod and pathScope
List entitled servicesGET /v1/servicesservices.read
Field schemaGET /v1/estamp/schema/:serviceCodeestamp.read
StatesGET /v1/estamp/statesestamp.read
Instruments for a stateGET /v1/estamp/states/:stateCode/instrumentsestamp.read
ArticlesGET /v1/estamp/instruments/:instrumentCode/articlesestamp.read
Calculate dutyPOST /v1/estamp/duty/calculateestamp.duty.calculate
Create orderPOST /v1/ordersorders.create
Attach partiesPOST /v1/estamp/orders/:orderId/partiesestamp.parties.write
Create payment intentPOST /v1/payments/intentspayments.write
Order status and paymentGET /v1/orders/:id/status, GET /v1/orders/:id/paymentorders.read
DocumentsGET /v1/orders/:id/documents and .../:documentId/downloaddocuments.read

The deeper per-endpoint reference lives in the e-Stamp docs, and a product-neutral walkthrough is in the e-Stamp API order flow guide.

How do you set up auth, scopes and environments?

Every protected endpoint takes Authorization: Bearer <key>, and the x-api-key header is also accepted. Keys are environment-scoped: sk_test_... for sandbox and sk_live_... for production. Each key carries scopes, and a valid key without the scope for an endpoint receives a 403 with code FORBIDDEN. Sensitive scopes can additionally be restricted to an IP allowlist, per the authentication docs.

For a rental flow you need these scopes in total: services.read, estamp.read, estamp.duty.calculate, orders.create, estamp.parties.write, payments.write, payments.read, orders.read and documents.read. That is a lot for one key, which is a reason to split your backend into two services. A "quote" service that only ever needs the read and calculate scopes can serve the front-end estimate screen. An "issuance" service holds the scopes that create orders and move money. If the quote key leaks, nobody can create an order from it.

Sandbox versus production

You can get a sandbox key instantly through self-signup; production keys require a KYC-approved organisation. Build and test the whole flow with sk_test_ first, then request production access. The sandbox vs production guide covers promotion, and the quickstart gets you a first call in minutes. Keep keys in environment variables or a secret manager, and never ship them in a mobile app or browser bundle. Your broker app's mobile client should talk to your backend, never to the API directly.

A small client you can reuse

The snippets in this guide use one thin wrapper so the interesting parts stay visible. The base URL is not repeated in the endpoint reference, so read it from the docs and keep it in configuration as STAMPMITRA_BASE_URL.

JavaScript
// Node.js 18+ (global fetch)
const BASE = process.env.STAMPMITRA_BASE_URL;
const KEY = process.env.STAMPMITRA_API_KEY; // sk_test_... in sandbox

export async function sm(method, path, { body, idempotencyKey } = {}) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    const e = json.error || {};
    const err = new Error(e.message || `HTTP ${res.status}`);
    err.status = res.status;
    err.code = e.code;
    err.requestId = e.requestId; // log this, quote it to support
    throw err;
  }
  return json;
}
Python
# Python 3.9+, requests
import os, requests

BASE = os.environ["STAMPMITRA_BASE_URL"]
KEY = os.environ["STAMPMITRA_API_KEY"]  # sk_test_... in sandbox

class SmError(Exception):
    def __init__(self, status, code, message, request_id):
        super().__init__(message)
        self.status, self.code, self.request_id = status, code, request_id

def sm(method, path, body=None, idempotency_key=None):
    headers = {"Authorization": f"Bearer {KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    r = requests.request(method, BASE + path, json=body, headers=headers, timeout=30)
    data = r.json() if r.content else {}
    if not r.ok:
        e = data.get("error", {})
        raise SmError(r.status_code, e.get("code"), e.get("message"), e.get("requestId"))
    return data

Both wrappers read the normalised error envelope, { "error": { "code", "message", "requestId" } }, which every endpoint in this API returns.

What do you need to collect before calling anything?

Many failed e-stamp orders are really data-collection failures upstream. Decide what your app must hold before the first API call, and make the form reject incomplete data early.

Deal data

You need the state where the property is located, the type of agreement (rental or leave-and-licence, whichever your users call it), the term, the rent and deposit figures, the start date and the property address. Whether the duty depends on rent, deposit, term, or some mix is a state-level rule. Do not guess it in your UI. Collect the figures, then let the schema endpoint and the duty calculation tell you which inputs the API wants.

Party data

For a residential tenancy the parties are normally the landlord (owner), the tenant, and sometimes a witness or a power-of-attorney holder acting for an absent owner. The docs describe the parties endpoint as attaching first-party, second-party and witness details to an order, and as attaching or correcting customer details. Collect legal names exactly as they appear on identity documents, a mobile number and an email for each, and the address you will print on the agreement.

If your product already verifies identities, run those checks before ordering the stamp. A mismatched name on a stamped document is expensive to fix, because the stamp is already issued. The PAN verification guide shows how to match a name against PAN records, and the KYC for fintech and NBFC use case covers the broader onboarding pattern. When a landlord is a company or an LLP, the business verification guide covers CIN and GSTIN checks.

Consent and minimisation

You are collecting personal data on behalf of two people who may never see your privacy notice unless you show it. As general good practice under the Digital Personal Data Protection Act 2023, tell each party what you collect, why (to prepare and stamp this agreement), and how long you keep it, and collect only what the flow needs. Do not log full identity numbers in application logs. The developer privacy policy and data processing addendum describe how StampMitra handles data on its side. I have not verified specific section numbers of the Act for this guide, so ask counsel what your own notice and retention periods should say.

How do you pick the state, instrument and article?

State, instrument and article come from lookup endpoints, not from a hardcoded dropdown. Rates and article numbering are state matters that change, which is exactly why you should not copy them into your app.

Step 1: confirm your entitlement

GET /v1/services lists every service your organisation is entitled to, verification and e-Stamp together. The documented example response contains a service entry like this:

cURL
curl -s "$STAMPMITRA_BASE_URL/v1/services" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY"
JSON
{
  "services": [
    { "serviceCode": "estamp.instrument.rent_agreement", "name": "Rent Agreement e-Stamp", "active": true }
  ]
}

Match on serviceCode and check active. There is no e-Stamp-specific catalogue endpoint; e-Stamp instruments surface as ordinary service entries once your organisation is entitled. If the entry you expect is missing, you are not entitled to it yet, which is a commercial or onboarding matter rather than a code bug. Cache the list for a short period, and have the app disable the "stamp this agreement" button when the entry is absent instead of letting the user fill a long form first.

Step 2: read the schema

GET /v1/estamp/schema/:serviceCode returns the input field schema. The docs state that e-Stamp has exactly one schema regardless of the serviceCode value, so you can pass the rent-agreement code you got from the catalogue. Use it to render or validate form fields rather than hardcoding them.

Step 3: states, instruments, articles

cURL
curl -s "$STAMPMITRA_BASE_URL/v1/estamp/states" -H "Authorization: Bearer $STAMPMITRA_API_KEY"
curl -s "$STAMPMITRA_BASE_URL/v1/estamp/states/<stateCode>/instruments" -H "Authorization: Bearer $STAMPMITRA_API_KEY"
curl -s "$STAMPMITRA_BASE_URL/v1/estamp/instruments/<instrumentCode>/articles" -H "Authorization: Bearer $STAMPMITRA_API_KEY"

Two quirks are worth knowing, both stated in the docs. There is no separate instrument entity distinct from an article: the instruments endpoint returns the same state-scoped article catalogue as the articles endpoint. And in the articles route, instrumentCode is treated as a state code, with no article sub-breakdown below the state-scoped list. In practice, your app lists states, then lists that state's articles, and the user (or your matching logic) picks the article that corresponds to a rental or leave-and-licence agreement. These lookups are served from StampMitra's synced master-data catalogue.

Choosing the article safely

Do not auto-select by string match on a label like "Rent". State catalogues use different names, and a broker app that silently picks the wrong article produces a stamp for the wrong instrument. Show the user the article name returned by the API and ask them to confirm, or build a reviewed mapping table owned by someone who understands the state's instruments, and log which article was used on each order. If the right article is not obvious, a person with legal knowledge should decide, not your code.

How does duty calculation work, and what do you never do?

Call POST /v1/estamp/duty/calculate with the scope estamp.duty.calculate. The docs call this the one authoritative pricing calculation: duty plus service charges, never recalculated or overridden client-side. It is metered against your fulfilment wallet, so each call costs ₹5, and an uncontrolled "recalculate on every keystroke" screen would burn money.

What the request carries

The e-Stamp field shape is not yet documented as a fixed contract, so build the request body from what the schema endpoint returns for your state and article. Conceptually it needs the state, the article, and the transaction values the schema asks for. Keep the body construction in one function:

JavaScript
import { sm } from "./sm.js";

// buildDutyInput() maps your deal record to the fields the schema asks for.
// Field names come from GET /v1/estamp/schema/:serviceCode, not from this guide.
export async function quote(deal) {
  const input = buildDutyInput(deal);
  return sm("POST", "/v1/estamp/duty/calculate", { body: input });
}
cURL
curl -s -X POST "$STAMPMITRA_BASE_URL/v1/estamp/duty/calculate" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  -H "Content-Type: application/json" \
  --data @duty-input.json

Here duty-input.json is the file your buildDutyInput wrote. This guide deliberately does not print a duty response with numbers, because the amounts depend on the state, article and values, and the API is the only place they should come from.

What you do with the answer

Show the returned amount to the user as the cost to be paid, along with the figures you sent, and keep the calculation response on the order record. Use these rules:

  1. Never add or subtract anything on top of the returned amount. If you charge your own platform fee, show it as a separate line.
  2. Treat the quote as valid for the inputs you sent. If the user changes rent, term, state or article, calculate again.
  3. Do not compare against a rate you read on a blog or a state website and "correct" the API. If you think a number looks wrong, contact support with the requestId and your inputs.
  4. Do not store a duty rule table in your database to pre-compute estimates. If you need a quick estimate screen, call the API once the user finishes the form.

Cost control

Debounce quote calls, calculate when the user presses "Continue" rather than on every field change, and cache the result keyed by a hash of the inputs for a short time. Sandbox has quota wording in your dashboard, so check it before load-testing quotes, since rate limits apply per endpoint (the docs' example 429 mentions 60 requests per 60 seconds; read your live limits rather than assuming).

How do you create the order and attach parties?

Orders are created through the generic POST /v1/orders, which works for any entitled serviceCode, including e-Stamp. The scope is orders.create. The documented parameters are externalOrderId (your own reference, required), externalCustomerId (your reference for the customer, required), serviceCode (required), details (free-form, optional) and an optional Idempotency-Key header.

The documented request

cURL
curl -s -X POST "$STAMPMITRA_BASE_URL/v1/orders" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: homeleaf-lease-88123-v1" \
  -d '{
    "externalOrderId": "ord_ext_123",
    "externalCustomerId": "cust_ext_456",
    "serviceCode": "estamp.instrument.rent_agreement",
    "details": { "state": "MH", "stampValue": 500, "note": "Shape not finalized - illustrative only" }
  }'

That body is the example from the order documentation, including its own warning: the details shape for e-Stamp is not finalized, and there are no first-class state, article or parties[] fields on the order. So in your code, treat details as an adapter output. Write one function that turns your internal deal record into the details object, give it unit tests, and reference it from nowhere else. When the contract firms up, you change one file.

Python
def create_stamp_order(deal):
    return sm(
        "POST", "/v1/orders",
        body={
            "externalOrderId": deal["order_ref"],          # e.g. "ord_ext_123"
            "externalCustomerId": deal["customer_ref"],    # e.g. "cust_ext_456"
            "serviceCode": "estamp.instrument.rent_agreement",
            "details": build_details(deal),                # adapter, see schema endpoint
        },
        idempotency_key=f"order-{deal['order_ref']}-v1",
    )

The documentation does not list the order creation response fields in this data file, so read the id your integration needs from a sandbox response and keep it next to your externalOrderId. The get-order and status endpoints take "its B2B-assigned id", meaning the one the API assigns, not your external reference.

Attaching parties

After the order exists, call POST /v1/estamp/orders/:orderId/parties with the scope estamp.parties.write. The docs describe it as attaching or correcting customer or party details on an existing e-Stamp order, and it is metered against your fulfilment wallet. Because it can also correct details, it is the right call when a user spots a typo before payment. The body follows the schema for e-Stamp, so build it through an adapter in the same way as details.

cURL
curl -s -X POST "$STAMPMITRA_BASE_URL/v1/estamp/orders/$ORDER_ID/parties" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  -H "Content-Type: application/json" \
  --data @parties.json

Make the party step an explicit review screen. Show the landlord's and tenant's names, addresses and contact details exactly as you will send them, and require an explicit "these are correct" confirmation. Then make a correction after payment an exceptional path handled by a human, since a stamp already issued cannot be edited in your database.

How do you collect payment?

Payment is separate from the order. Create a payment intent with POST /v1/payments/intents (scope payments.write, optional Idempotency-Key). The docs describe it as a Razorpay-backed payment intent for an order. To check it, call GET /v1/payments/intents/:id (scope payments.read), or fetch the payment attached to an order with GET /v1/orders/:id/payment (scope orders.read).

The documentation here does not define the intent request body or its response, so take those field names from the live payments documentation or a sandbox call and wrap them behind a function like createPaymentIntent(order).

cURL
curl -s -X POST "$STAMPMITRA_BASE_URL/v1/payments/intents" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pay-ord_ext_123-attempt-1" \
  --data @intent.json

Who pays: the party, you or the broker?

That is a product decision, not an API setting. Three common models exist. The user pays at checkout, either tenant or landlord according to your commercial terms and local custom. The broker pays and recovers it through brokerage. Or you pay as the platform and absorb it into a subscription. Whatever you choose, make the displayed payable amount equal to the API-calculated duty and charges, with your own fee as a separate visible line.

Separate the two kinds of money

Two things are easy to blur. The payment intent collects money for the order. Separately, several e-Stamp calls (duty calculation, parties) are metered against your fulfilment wallet, and a wallet shortfall returns 402 with PAYMENT_REQUIRED and the message "Insufficient fulfilment wallet balance to cover this call." Monitor the wallet balance and alert your finance owner before it runs dry, because a 402 in the middle of a user's checkout is the kind of failure users remember.

Do not trust the browser

If your front end opens a payment sheet, the "payment successful" callback in the browser is a hint, not proof. Your server should confirm by reading the payment state (GET /v1/payments/intents/:id or GET /v1/orders/:id/payment) before it tells the user the stamp is on its way. That also protects you when the user closes the tab mid-payment.

How do you track the order and receive the document?

After payment confirms, fulfilment runs asynchronously. Poll the order with GET /v1/orders/:id/status (scope orders.read), or read the full order with GET /v1/orders/:id. When the order completes, list its documents with GET /v1/orders/:id/documents and request a download URL with GET /v1/orders/:id/documents/:documentId/download, both under documents.read. The docs describe the download URL as typically short-lived and signed.

Polling pattern

Webhooks exist as a concept: the webhooks page lists order lifecycle events such as order.created, order.payment_confirmed, order.fulfilment_started, order.fulfilment_completed, order.failed, order.cancelled, order.refund_completed and order.document_ready. But that page is marked coming soon, and notes that endpoints are configured internally by StampMitra, with no public self-serve registration route today. So build on polling now, and design your order state machine so a webhook can be added as a second trigger later without rewriting it.

JavaScript
async function waitForCompletion(orderId, { maxMs = 15 * 60_000 } = {}) {
  const started = Date.now();
  let delay = 2000;
  while (Date.now() - started < maxMs) {
    const s = await sm("GET", `/v1/orders/${orderId}/status`);
    // Persist the raw status response; branch on the values you observe in sandbox.
    await saveStatus(orderId, s);
    if (isTerminal(s)) return s;
    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 1.7, 30_000);
  }
  return { timedOut: true };
}

The data file does not enumerate the status values, so isTerminal must be written against the values you see in your sandbox and confirm in the docs. Do not guess them from the event names. Also, a poll timeout is not a failure: it means "ask again later", so schedule a background job rather than holding the user's request open.

Fetching and saving the certificate

JavaScript
async function archiveDocuments(orderId) {
  const docs = await sm("GET", `/v1/orders/${orderId}/documents`);
  // For each document in the listing, request a fresh signed URL and copy the bytes.
  for (const d of docs.documents ?? docs) {
    const link = await sm("GET", `/v1/orders/${orderId}/documents/${d.id}/download`);
    await copyToOwnStorage(orderId, d, link); // stream to your bucket right away
  }
}

The listing and download response field names are not enumerated in the documentation data, so the documents array and id above are placeholders to confirm against a sandbox response. Request the download link at the moment you copy it, never earlier, since signed URLs expire.

What fails, and how do you handle retries safely?

All errors share one shape, { "error": { "code", "message", "requestId" } }. The same shape is described in the API errors guide. For a rental flow, these are the cases that matter.

HTTPCodeTypical cause in this flowWhat to do
400BAD_REQUESTMissing or malformed field in the requestShow the message to a developer, not a tenant. Fix the input. Do not retry unchanged.
401UNAUTHORIZEDMissing or invalid API keyCheck the key and environment (sk_test_ vs sk_live_).
402PAYMENT_REQUIREDFulfilment wallet cannot cover the callTop up, then retry. Alert finance.
403FORBIDDENKey valid but lacks the scopeAdd the scope, or use the right service key.
404NOT_FOUNDWrong order id, or using your external id where the API id is expectedUse the API-assigned id.
429TOO_MANY_REQUESTSRate limit hitBack off and retry with jitter.
502INTERNAL_ERRORUpstream service unreachableRetry with backoff, then escalate if it persists.

The 400, 402, 403, 404 and 429 bodies appear in the docs examples. The 502 example shown in the docs is for verification endpoints ("Unable to reach the order/customer service right now"), so expect a similar upstream failure on order endpoints but confirm in sandbox.

Making retries idempotent

Two endpoints accept an optional Idempotency-Key: order creation and payment intents. Use it. Derive the key from something stable in your system, such as order-<your-order-ref>-v1, never from a timestamp or a random value generated per attempt, which would defeat its purpose. The key stops a network timeout from becoming two orders for one agreement. Also use your externalOrderId as a uniqueness constraint in your own database, so even without the header you detect that you already have an order for this agreement before you create another.

A retry decision table

SituationSafe to retry?How
Timeout on order creationYesSame Idempotency-Key, same body
429 on any callYesExponential backoff with jitter
402 on duty or partiesOnly after top-upFix the wallet first
400 validationNoCorrect the data
Timeout on payment intentYes, with same keyThen read intent state before creating anything new
Payment taken, order not progressingDo not pay againRead GET /v1/orders/:id/payment and /status, then contact support with the requestId

Log the requestId from every error. It is the single most useful thing to include when you write to developer support, along with the environment and your externalOrderId.

How do you store issued documents and records?

Treat the issued e-Stamp certificate as a legal record that your users will want years later, for example for a dispute, a loan or a sale of the property. Your platform will probably outlive any single download link, so the copy in your storage is the one that matters.

What to store

Per order, store your own externalOrderId and the API-assigned order id, the state, instrument or article used, the inputs sent to duty calculation, the calculation response as received, the parties as submitted, the payment reference, timestamps for each status change, and the raw status responses. For each document, store the file bytes, a checksum, the original document id, and when you fetched it.

Where to store it

Put files in an object store with encryption at rest, private-by-default access, and per-tenant path prefixes. Serve them to users via your own authenticated endpoint that issues short-lived links, rather than making objects public. Give the landlord, the tenant and the broker access according to your product rules, and keep an access log.

How long to keep it

Retention is a legal and business question, and the right answer depends on the document type, your contracts with users and the law applicable to you. I have not verified a statutory retention period for rental records, so do not invent one in your policy. Ask counsel, write the decision down, and implement deletion so that it actually runs. StampMitra's own handling is described in its data retention and deletion schedule; your own obligations to your users are separate.

Avoid common storage mistakes

Do not store the signed download URL as the record. Do not store the document only in a user's inbox via email. Do not overwrite a document when a corrected order is issued, because the old stamp still exists. Keep versions, and link the new order to the old one in your own data.

Who is responsible for registration?

This section is deliberately cautious. E-stamping pays the stamp duty on the instrument. Registration is a separate legal step governed by registration law and, for tenancies, often by a state rent law, and the two should not be confused in your product copy.

What I could and could not verify

I tried to read the official texts for this guide on 2026-10-16: the India Code copy of the Registration Act 1908, the Ministry of Housing and Urban Affairs copy of the Model Tenancy Act 2021, and the Maharashtra Inspector General of Registration portal. Those official sites returned access errors or were unavailable at the time, so I did not rely on them and I am not quoting section numbers, term thresholds, deadlines, penalties or fees from them.

What I could open was a summary by PRS Legislative Research, an independent think tank rather than a government source. It says the Model Tenancy Act 2021 was approved by the Union Cabinet on 2 June 2021 as a model for adoption by states and union territories, that it proposes a written agreement between landlord and tenant, and that the Rent Authority is to be told about the agreement within two months. It also notes that housing is a State subject and that the model's effect depends on each state adopting it. Treat that as a pointer to a policy direction, not a statement that your state has this rule.

What this means for your product

  1. Rules differ by state. Some states have their own rent control or tenancy statutes, some have adopted tenancy reforms, and the registration requirement can depend on the term of the tenancy and on the kind of arrangement (lease or licence).
  2. A stamped agreement may still need registration, and in some states a tenancy may need to be reported to a rent authority or similar body in addition. E-stamping does not do that for you.
  3. The responsible party (landlord, tenant, or both) is a matter for the law of the state and the terms of the agreement. Do not hardcode "landlord registers" in your UI as a fact.
  4. Where available, state registration departments publish their own procedures, forms and fees. Link to the relevant state department from your app, so the user sees the current official guidance rather than your copy of it.

How to design around it

Add a plain "What happens after stamping" step to your flow. Tell users, in general terms, that their agreement may need registration or reporting depending on the state and the term, and that they should confirm with a qualified lawyer or the state's registration office. Do not tell them it is optional, and do not tell them it is mandatory, unless your counsel has signed off the exact wording for each state you serve. Record that the user saw the notice. If your business offers registration assistance, that is a separate service with its own legal review. This guide covers only the stamping integration.

When should a proptech use this, and when not?

Use e-stamping through an API when your app already holds the structured deal data, when volume makes manual stamp-paper handling a bottleneck, when you need to bind order references to your own records, and when you want one audit trail covering identity checks, stamping and document storage.

Do not use it when the state or instrument you need is not in the lookups for your entitlement, when users need a bespoke instrument no catalogue article covers, when the parties are not ready to commit (stamp only after terms are final, because duty is paid up front), or when you cannot yet staff a support path for failed orders. Also avoid it as a way to give "legal certainty" you cannot back: e-stamping is a step, not a guarantee that the agreement is enforceable or complete.

QuestionIf yesIf no
Is the state and article returned by the lookups for your org?ContinuePause, contact support
Are terms final and both parties' data verified?Create the orderKeep the draft unstamped
Is your counsel comfortable with your registration notice per state?Launch in that stateLaunch only where reviewed
Do you have a wallet top-up alert?Go liveAdd one first

How do you test in sandbox and go to production?

Start with a sandbox key from self-signup. Run each step by hand with cURL, then script it, then put it behind your adapter functions. The sandbox lets you discover real response shapes for the places this guide marks as placeholders: the order creation response, payment intent body, status values and document listing.

A test plan

  1. Entitlement: call GET /v1/services and assert your rent-agreement code is present and active.
  2. Lookups: list states, pick one, list its articles, and confirm your mapping picks something sensible.
  3. Quote: send one valid and one invalid input to duty calculation and assert you handle both, including the 400 body.
  4. Order: create it twice with the same Idempotency-Key and assert you end up with one order.
  5. Parties: attach, then correct, and re-read the order.
  6. Payment: create an intent, interrupt it, and confirm your server reads the state rather than trusting the browser.
  7. Completion: poll to a terminal status and download the documents into your own storage.
  8. Failures: remove a scope from a test key to see the 403, and use a bad key to see the 401. Check the errors guide for handling patterns.

Going live

Production keys require a KYC-approved organisation, so start that process early because it is not instant. Switch your configuration from sk_test_ to sk_live_, keep the two environments' data separate, set up IP allowlisting for sensitive scopes if you use it, run one real low-risk order end to end, and have a named person watch the first day's orders. Review the API acceptable use policy and the security policy before launch. If you are building with an AI assistant, the MCP server and build-with-AI page are useful, but review any generated code for the idempotency and storage rules above.

How do you troubleshoot common problems?

"My order creation returns 400"

Read error.message and compare your body against the schema endpoint. Because details is free-form and not finalised, the most common cause is a field your state or article expects that your adapter did not populate. Fix the adapter, not the retry loop.

"I get 403 on parties or payments"

The key is valid but lacks estamp.parties.write or payments.write. Scopes are per key, so a key made for the quote service will not work for issuance.

"Duty calculation worked yesterday and now returns 402"

The call is metered against the fulfilment wallet. A 402 with PAYMENT_REQUIRED means the balance cannot cover it. Top up, then retry.

"The order is stuck"

First confirm payment through GET /v1/orders/:id/payment. If payment is confirmed and the status has not moved for longer than you consider reasonable, do not create a new order, because you would pay twice. Gather the order id, your externalOrderId, the status response and the requestId of your last call, and write to [email protected].

"The download link stopped working"

The link is short-lived and signed. Request a new one from the download endpoint, and make sure your job copies the file immediately after the document appears.

"Two orders exist for one agreement"

Usually a retry without an idempotency key, or a key that changed per attempt. Add a unique constraint on your external order reference, and derive the key from it.

How StampMitra Helps

StampMitra provides the e-Stamp surface described above as real, documented endpoints: a service catalogue through GET /v1/services, a schema endpoint, state, instrument and article lookups served from StampMitra's synced master data, an authoritative duty and charge calculation, order creation through the generic orders API, party attachment, Razorpay-backed payment intents, order status, and document listing with signed download URLs. Every call is authenticated with scoped API keys, errors come back in one consistent envelope with a requestId, and sandbox keys are available through self-signup so you can build before you commit. Pricing for e-Stamp calls is on the pricing page, currently ₹5 per call for the e-Stamp family.

Being plain about limits matters here. StampMitra is a private LegalTech platform operated by Bani Global Industries LLP, not a government body or a law firm. It does not advise on whether your agreement needs registration, does not register documents for you through this API, and does not guarantee that any particular agreement will be accepted or enforced. The e-Stamp details shape for orders is documented as not finalised, and there are no dedicated first-class state, article or parties[] fields on the order yet, so the adapter approach in this guide is the right way to insulate your code. Webhooks are not self-serve today, so polling is the supported pattern.

On the roadmap side, these are plans, not commitments or dates: a more complete, typed e-Stamp order contract, self-service webhook registration so your app can react to events such as order.document_ready without polling, and richer examples for rental and leave-and-licence flows. If one of those matters to your launch, tell us through support or the contact page, since real integration needs shape what gets built first.

To begin, create a sandbox account, follow the quickstart, and read the e-Stamp docs. The Postman collection and SDKs pages list other ways to explore the API. For key handling, see API key authentication.

API reference at a glance

e-Stamp Order Creation: paramsPOST /v1/orders

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
Idempotency-KeyheaderOptional idempotency key.
externalOrderIdrequiredstringYour own reference for this order.
externalCustomerIdrequiredstringYour own reference for the customer.
serviceCoderequiredstringThe entitled service to order (e.g. an estamp.* code).
detailsobjectFree-form service-specific payload (e-Stamp field shape is not finalized).
e-Stamp Order Creation: responsePOST /v1/orders
{
  "externalOrderId": "ord_ext_123",
  "createdAt": "2026-10-15T09:41:07.000Z",
  "serviceCode": "estamp.instrument.rent_agreement"
}
e-Stamp Order Creation: pricePOST /v1/orders

e-Stamp Order Creation: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).

Duty / Value Calculation: paramsPOST /v1/estamp/duty/calculate

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
stateCoderequiredstringState code, e.g. MH.
articleCoderequiredstringStamp article code from the articles lookup.
stampDutyAmountrequirednumberStamp duty amount to calculate charges for; must be positive.
districtCodestringDistrict code, where the state needs one.
sroValuestringSub-registrar office value, where the state needs one.
doorstepDeliverybooleanWhether doorstep delivery is requested.
couponCodestringCoupon code to apply.
masterDataVersionintegerMaster-data version the caller last read.
Duty / Value Calculation: pricePOST /v1/estamp/duty/calculate

Duty / Value Calculation: ₹5 API fee per successful call, pay-as-you-go (lower on Growth and Scale).

Frequently asked questions

How do I e-stamp a rental agreement through the StampMitra API?

Check your entitlement with GET /v1/services, resolve state and article through the /v1/estamp lookups, calculate duty with POST /v1/estamp/duty/calculate, create an order with POST /v1/orders, attach parties, pay, then download the certificate.

Does the API tell me the stamp duty for a rent agreement?

Yes. POST /v1/estamp/duty/calculate runs the authoritative duty and service charge calculation, and the response amount is not recalculated or overridden client-side. Your app should never hardcode duty rates, because they vary by state and instrument.

Which scopes does a rental e-stamp integration need?

The documented scopes are services.read, estamp.read, estamp.duty.calculate, orders.create, estamp.parties.write, payments.write, payments.read, orders.read and documents.read. Splitting quote and issuance across separate keys limits the damage if one key leaks.

How do I avoid creating duplicate e-stamp orders on retries?

Send an Idempotency-Key header on POST /v1/orders and POST /v1/payments/intents, derived from a stable value such as your own order reference. Also keep your externalOrderId unique in your database so you detect an existing order before creating another.

Are webhooks available for e-stamp order updates?

The webhooks page lists order lifecycle events but marks self-serve webhooks as coming soon, with endpoints configured internally by StampMitra today. Until then, poll GET /v1/orders/:id/status with exponential backoff and design your state machine to accept webhooks later.

How should I store the issued e-stamp certificate?

Download it as soon as the order completes and copy the file into your own encrypted storage with a checksum and the order references. Download URLs are typically short-lived and signed, so never store the URL itself as your record.

Does e-stamping mean my rental agreement is registered?

No. E-stamping deals with stamp duty. Whether a rental or leave-and-licence agreement also needs registration or reporting, and who is responsible, differs by state and term. Check with qualified counsel and the state registration department; StampMitra does not give legal advice.

What is the shape of the details object for an e-stamp order?

The orders documentation describes details as free-form and states the e-Stamp field shape is not finalized upstream. Build it through a single adapter function using the schema endpoint, and re-check the docs before each release.

What does a 402 error mean during an e-stamp flow?

A 402 with code PAYMENT_REQUIRED means your fulfilment wallet balance cannot cover the metered call, such as duty calculation or attaching parties. Top up the wallet and retry, and set up a low-balance alert so users never hit it at checkout.

Can I test the e-stamp flow without real money?

Yes, you can get a sandbox key (sk_test_) instantly through self-signup and run the flow end to end. Production keys (sk_live_) require a KYC-approved organisation, so start that process before your launch date.

Sources

  • StampMitra Developers, e-Stamp API reference pages: /docs/estamp, /docs/auth, /webhooks, /pricing. Endpoint paths, scopes, parameters and error shapes in this guide come from these pages, verified 2026-10-16.
  • PRS Legislative Research, "The Model Tenancy Act, 2021" (prsindia.org/billtrack/the-model-tenancy-act-2021), opened and read 2026-10-16. A non-governmental summary, used only for the points attributed to it above.
  • Registration Act, 1908 (India Code), Model Tenancy Act, 2021 (Ministry of Housing and Urban Affairs) and the Maharashtra Inspector General of Registration portal: attempted on 2026-10-16, but the sites returned access errors or were unavailable, so no claims are taken from them. Readers should consult these official sources and their state's registration department directly.
  • Digital Personal Data Protection Act, 2023: referenced only as general good practice, with no section citations; not re-verified for this guide.