StampMitraDevelopers
Official SDK

Node.js / TypeScript SDK

The official Node.js and TypeScript client for the StampMitra Developer API: identity verification and e-Stamp with typed responses, automatic retries and signed-webhook helpers.

Pre-release 0.1.0 - package registry publishing in progress

Package
@stampmitra/sdk
Version
0.1.0
Requires
Node.js 18 or newer (uses the global fetch). The core also runs on Bun, Deno and Cloudflare Workers. Zero runtime dependencies, dual ESM/CJS, fully typed.
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

npm
npm install @stampmitra/sdk

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

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.

TypeScript
import { StampMitra } from '@stampmitra/sdk';

const client = new StampMitra(); // reads STAMPMITRA_API_KEY
const res = await client.verify.pan({ pan: 'ABCDE1234F', name: 'Rahul Kumar' });
console.log(res.referenceId, res.valid, res.nameMatchResult);
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 apiKey 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 allowCustomKey: true. In a browser runtime the constructor refuses sk_live_ keys and warns for test keys.

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
new StampMitra({
  apiKey,
  baseUrl: 'https://api-b2b.stampmitra.in',
  timeout: 30_000,
  maxRetries: 2,
  fetch,
  defaultHeaders,
  appInfo: { name: 'my-app', version: '1.0.0', url: 'https://example.com' },
  logger,
  validateInput: true,
  allowCustomKey: false,
});

// Every method also takes per-request options as its last argument:
// { timeout, maxRetries, idempotencyKey, headers, signal }
Resources

Every resource in one place

Response bodies are typed from the API examples. Payloads whose shape the API has not finalised (e-Stamp details, duty, parties, payment intent) are typed as open objects.

All resources
// Verification (POST /v1/verify/*). Every call, including negative results, is billed.
await client.verify.pan({ pan: 'ABCDE1234F' });
await client.verify.gstin({ gstin: '07AAAAA1111A1Z1', businessName: 'Jane Doe Enterprises' });
await client.verify.rc({ vehicleNumber: 'MH12AB1234' });
await client.verify.voterId({ voterId: 'ABC1234567' });
await client.verify.drivingLicense({ dlNumber: 'MH1420110012345', dob: '1990-01-01' }); // IP-allowlisted key
await client.verify.passport({ fileNumber: 'PA1079341954215', dob: '1990-01-01' }); // IP-allowlisted key
await client.verify.cin({ cin: 'U74999DL2015PTC284009' });
await client.verify.udyam({ udyamNumber: 'UDYAM-MH-12-1234567' });
await client.verify.panToGstin({ pan: 'ABCDE1234F' });
await client.verify.panToUdyam({ pan: 'ABCDE1234F' });
await client.verify.bankAccount({ accountNumber: '000123456789', ifsc: 'ICIC0001234', name: 'Ravi Kumar' });

// Services and e-Stamp reference data
await client.services.list();
await client.services.get('estamp.instrument.rent_agreement');
await client.estamp.getSchema('estamp.instrument.rent_agreement');
await client.estamp.listStates();
await client.estamp.listInstruments('MH');
await client.estamp.listArticles('INSTRUMENT_CODE');
await client.estamp.calculateDuty({ /* see e-Stamp docs */ });

// Orders and payments
const order = await client.orders.create({
  externalOrderId: 'o1',
  externalCustomerId: 'c1',
  serviceCode: 'estamp.instrument.rent_agreement',
  details: {},
});
await client.estamp.addParties('ORDER_ID', { /* parties */ });
await client.orders.get('ORDER_ID');
await client.orders.getStatus('ORDER_ID');
await client.orders.getPayment('ORDER_ID');
await client.orders.listDocuments('ORDER_ID');
await client.orders.getDocumentDownload('ORDER_ID', 'DOCUMENT_ID'); // short-lived signed URL
await client.payments.intents.create({ /* see payment docs */ });
await client.payments.intents.get('INTENT_ID');

// Usage, logs, webhook deliveries
await client.usage.summary();
await client.logs.list({ service: 'estamp', status: 'failure', limit: 50 });
await client.webhooks.events.list({ limit: 20 });
await client.webhooks.events.replay('EVENT_ID');

// Raw response access
const { data, response } = await client.verify.pan({ pan: 'ABCDE1234F' }).withResponse();
console.log(response.status, response.requestId, response.attempts);
Pagination

Pagination

listAll follows nextCursor for you; list returns a single page with items and nextCursor.

Pagination
for await (const entry of client.logs.listAll({ service: 'estamp' })) console.log(entry.id);

const page = await client.logs.list({ limit: 50 }); // page.items, page.nextCursor

for await (const delivery of client.webhooks.events.listAll()) {
  // ...
}
Errors

Error classes

Every error extends StampMitraError and carries status, code, message, requestId and raw. ValidationError adds field and RateLimitError adds retryAfter (seconds). Errors are mapped by HTTP status only, so non-JSON error bodies still produce the right class.

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.
403PermissionErrorThe 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 / timeout / abortConnectionError / TimeoutError / RequestAbortedErrorNo usable response: connection failure, timeout or cancellation.
TypeScript
import { ValidationError, RateLimitError } from '@stampmitra/sdk';

try {
  await client.verify.pan({ pan: 'ABCDE1234F' });
} catch (e) {
  if (e instanceof ValidationError) console.error('Bad input:', e.field, e.message);
  else if (e instanceof RateLimitError) console.log(e.retryAfter, e.requestId);
  else throw e;
}
Retries and idempotency

Retries and idempotency

Validation

Client-side validation

Inputs are trimmed, upper-cased and checked locally against the API formats (PAN, GSTIN, IFSC, CIN, Udyam, vehicle number, 9 to 18 digit bank account, passport file number, driving licence, voter ID, YYYY-MM-DD dates, name length). Invalid input throws ValidationError (local: true) with no network call and no billing. Turn it off with validateInput: 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.

Two entry points: @stampmitra/sdk/webhooks is async and uses WebCrypto (Next.js, Cloudflare Workers, Node 18+), and @stampmitra/sdk/webhooks-node is the synchronous Node variant. Stale timestamps (default tolerance 300 s) are rejected and comparison is constant-time.

TypeScript
// Edge runtimes, Next.js, Node 18+ (async, WebCrypto)
import { constructEventFromHeaders } from '@stampmitra/sdk/webhooks';
const event = await constructEventFromHeaders(rawBody, request.headers, secret);

// Synchronous Node variant
import { verifySignature, constructEvent } from '@stampmitra/sdk/webhooks-node';
const ok = verifySignature(rawBody, timestampHeader, signatureHeader, secret); // boolean

verifySignature returns a boolean and never throws on bad input. constructEvent throws WebhookSignatureError, whose reason is malformed, stale or mismatch.

Framework recipes

Framework recipes
import express, { type Request, type Response } from 'express';
import { constructEvent, WebhookSignatureError } from '@stampmitra/sdk/webhooks-node';

const app = express();

// IMPORTANT: verify against the RAW body, not re-serialised JSON.
app.post(
  '/webhooks/stampmitra',
  express.raw({ type: 'application/json' }),
  (req: Request, res: Response) => {
    try {
      const event = constructEvent(
        req.body as Buffer,
        String(req.header('X-Webhook-Timestamp')),
        String(req.header('X-Webhook-Signature')),
        process.env.STAMPMITRA_WEBHOOK_SECRET!,
      );
      console.log('event', req.header('X-Webhook-Event-Id'), event);
      res.sendStatus(200);
    } catch (err) {
      if (err instanceof WebhookSignatureError) return void res.status(400).send(err.reason);
      throw err;
    }
  },
);

app.listen(3000);

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

Safe logging

Safe logging

Nothing is logged by default. Pass logger: { debug(msg, meta) {} } to receive request and response records with Authorization and x-api-key redacted and PAN, GSTIN, account numbers and names masked.

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.