StampMitraDevelopers
Official SDK

Python SDK

The official Python client for the StampMitra Developer API: identity verification and e-Stamp with sync and async clients, typed pydantic models and signed-webhook helpers.

Pre-release 0.1.0 - package registry publishing in progress

Package
stampmitra
Version
0.1.0
Requires
Python 3.9 or newer. Runtime dependencies: httpx and pydantic 2. Sync and async clients, typed (py.typed, passes mypy --strict).
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

pip
pip install stampmitra

Available once published. Not yet published to PyPI. This command works once the package is released; until then follow the GitHub repository for the release.

pip (from GitHub)
pip install "git+https://github.com/Bani-Global-Industries-LLP/stampmitra-developer-python.git"

Until then, install from GitHub: The repository is a standard pyproject (hatchling, src layout), so pip can build it straight from Git.

Quickstart

Verify a PAN in five lines

Use a sandbox key (sk_test_...) while you build. It is free, never debits your wallet and runs against sandbox data.

Quickstart
from stampmitra import StampMitra

client = StampMitra()  # reads STAMPMITRA_API_KEY
result = client.verify.pan("ABCDE1234F", name="Rahul Kumar")
print(result.valid, result.registered_name, result.reference_id)
Authentication

API keys and environments

Sandbox

Sandbox keys start with sk_test_. They are free and return sandbox data.

Live

Live keys start with sk_live_. They need an approved production access request and are metered per successful call.

Pass api_key="sk_test_..." or set the STAMPMITRA_API_KEY environment variable. client.environment is "test" or "live". Keys that do not match ^sk_(test|live)_[A-Za-z0-9_-]{8,}$ are rejected unless you pass allow_custom_key=True.

Server-side only. Never ship sk_ keys in browser or mobile apps, or commit them to a repository. Keep them in environment variables or a secrets manager. Authentication
Client options
from stampmitra import StampMitra, RequestOptions

client = StampMitra(
    api_key="sk_test_...",
    base_url="https://api-b2b.stampmitra.in",
    timeout=30,
    max_retries=2,
    default_headers={},
    app_info={"name": "my-app", "version": "1.0.0", "url": "https://example.com"},
    debug=False,
    validate_input=True,
    allow_custom_key=False,
)

# Every method also takes per-request options:
client.verify.pan(
    "ABCDE1234F",
    options=RequestOptions(timeout=10, max_retries=1, idempotency_key="...", headers={}),
)
Resources

Every resource in one place

JSON field names are camelCase and model attributes are snake_case (registered_name). Unknown fields are preserved (model.model_extra, model.to_dict()). Request bodies for e-Stamp and orders are plain dicts exactly as the API documents them. Every returned model has .response with status_code, headers, request_id and content. Endpoints the spec leaves unconstrained return an ApiObject.

All resources
# verify (every call is billed, including negative results; HTTP 200 with valid=False)
client.verify.pan("ABCDE1234F", name="Rahul Kumar")
client.verify.gstin("07AAAAA1111A1Z1", business_name="Acme")
client.verify.rc("MH12AB1234")
client.verify.voter_id("ABC1234567")
client.verify.driving_license("MH1420110012345", "1990-01-01")  # IP-allowlisted key
client.verify.passport("PA1079341954215", "1990-01-01")  # IP-allowlisted key
client.verify.cin("U74999DL2015PTC284009")
client.verify.udyam("UDYAM-MH-12-1234567")
client.verify.pan_to_gstin("ABCDE1234F")
client.verify.pan_to_udyam("ABCDE1234F")
client.verify.bank_account("000123456789", "ICIC0001234", name="Jane Doe")

# services and e-Stamp
client.services.list()
client.services.get("estamp.instrument.rent_agreement")
client.estamp.get_schema(code)
client.estamp.list_states()
client.estamp.list_instruments("MH")
client.estamp.list_articles(instrument_code)
client.estamp.calculate_duty({...})
client.estamp.add_parties(order_id, {...})

# orders and payments
order = client.orders.create({"externalOrderId": "o1", "externalCustomerId": "c1", "serviceCode": code, "details": {}})
client.orders.get(id)
client.orders.get_status(id)
client.orders.get_payment(id)
client.orders.list_documents(id)
client.orders.get_document_download(id, document_id)
client.payments.intents.create({...})
client.payments.intents.get(intent_id)

# usage, logs, webhook events
client.usage.summary()
client.logs.list(from_="2026-10-01", to="2026-10-16", service="pan.verify", status="200", limit=50)
client.webhooks.events.list(limit=20)
client.webhooks.events.replay(event_id)
Pagination

Pagination

client.webhooks.events.iter_all() works the same way (it assumes the items / nextCursor shape, like /v1/logs).

Pagination
for entry in client.logs.iter_all(service="pan.verify", limit=100):  # follows nextCursor
    print(entry.id)

async for entry in async_client.logs.iter_all(limit=100):
    ...
Errors

Error classes

Every error extends StampMitraError (status, code, message, request_id, body). ValidationError has .field and RateLimitError has .retry_after (seconds). Non-JSON error bodies still map by status. The API error body is {"error": {"code", "message", "requestId", "field?"}}.

HTTP statusClassNotes
400 / 422ValidationErrorBad input. Also raised locally, before any request, by the validation helpers.
401AuthenticationErrorMissing, malformed or revoked API key.
402PaymentRequiredErrorInsufficient wallet balance or credit.
403PermissionDeniedErrorThe server message is passed through verbatim, e.g. Passport and Driving Licence need an IP-allowlisted key.
404NotFoundErrorUnknown order, service or event id.
409ConflictErrorIdempotency-Key reused with a different body, or a state conflict.
429RateLimitErrorBack off and retry; the server's Retry-After value is exposed when it is sent.
5xxUpstreamErrorA server-side or upstream failure. Retried automatically for 502, 503 and 504.
network / timeoutAPIConnectionError / APITimeoutErrorNo usable response: connection failure, timeout or cancellation.
Python
from stampmitra import StampMitraError, ValidationError, RateLimitError, PermissionDeniedError

try:
    client.verify.pan("ABCDE1234F")
except ValidationError as e:  # 400 (or local validation, e.status is None)
    print(e.field, e.message)
except RateLimitError as e:  # 429
    print(e.retry_after)
except StampMitraError as e:  # base class
    print(e.status, e.code, e.request_id)
Retries and idempotency

Retries and idempotency

Validation

Client-side validation

Inputs are normalised (trimmed, upper-cased) and checked against the same patterns as the backend before sending. Failures raise ValidationError locally, with no network call and no billing. Turn it off with StampMitra(validate_input=False).

Webhooks

Verify webhook signatures

Deliveries carry X-Webhook-Signature, X-Webhook-Timestamp (Unix seconds) and X-Webhook-Event-Id. The signature is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your endpoint secret. Always verify against the raw body, never re-serialised JSON.

The webhooks module verifies the signature in constant time and rejects timestamps more than tolerance_seconds (default 300) from now. The same helpers are on the client as client.webhooks.verify_signature and construct_event.

Python
from stampmitra import webhooks, WebhookSignatureError

event = webhooks.construct_event(
    raw_body,
    headers["X-Webhook-Timestamp"],
    headers["X-Webhook-Signature"],
    secret,
)
# webhooks.verify_signature(...) returns True or raises WebhookSignatureError

WebhookSignatureError exposes a code: signature_mismatch, timestamp_outside_tolerance, invalid_timestamp or missing_secret.

Framework recipes

Framework recipes
import os

from flask import Flask, request

from stampmitra import WebhookSignatureError, webhooks

app = Flask(__name__)
SECRET = os.environ["STAMPMITRA_WEBHOOK_SECRET"]


@app.post("/webhooks/stampmitra")
def receive() -> "tuple[str, int]":
    try:
        event = webhooks.construct_event(
            request.get_data(),  # the RAW body, not request.json
            request.headers.get(webhooks.TIMESTAMP_HEADER, ""),
            request.headers.get(webhooks.SIGNATURE_HEADER, ""),
            SECRET,
        )
    except WebhookSignatureError as err:
        return str(err), 400
    print("event", event.core_order_id, event.core_event_type, event.occurred_at)
    return "ok", 200

Webhooks: Event types, payloads and retry policy are on the Webhooks page.

Safe logging

Safe logging

Off by default. StampMitra(debug=True) logs to the stampmitra logger (or pass logger=). Authorization and x-api-key are redacted and PAN, GSTIN, account numbers and names are masked in logged bodies.

SDKs

Other ways to integrate

The REST API works from any language, and the other SDKs share the same contract.

Ready to call the API?

Get a free sandbox key and run the quickstart above in about 5 minutes.