StampMitraDevelopers
Official SDK

Java SDK

The official Java client for the StampMitra Developer API: identity verification and e-Stamp with a fluent builder, immutable typed models, automatic retries and signed-webhook helpers.

Pre-release 0.1.0 - package registry publishing in progress

Package
in.stampmitra:stampmitra-java
Version
0.1.0
Requires
Java 11 or newer (built with --release 11; tested on JDK 11, 17 and 21). The only runtime dependencies are Jackson (jackson-databind, jackson-datatype-jsr310). HTTP uses the JDK's built-in java.net.http.HttpClient, and you can plug in another one. No Lombok, no annotation processors.
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

Maven
<dependency>
  <groupId>in.stampmitra</groupId>
  <artifactId>stampmitra-java</artifactId>
  <version>0.1.0</version>
</dependency>
Gradle (Kotlin DSL)
implementation("in.stampmitra:stampmitra-java:0.1.0")

Available once published. Not yet published to Maven Central. 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.

Java
import in.stampmitra.sdk.StampMitra;
import in.stampmitra.sdk.models.PanVerification;

StampMitra client = StampMitra.builder().apiKey(System.getenv("STAMPMITRA_API_KEY")).build();
PanVerification pan = client.verify().pan("ABCDE1234F", "Rahul Kumar");
System.out.println((pan.isValid() ? "valid " : "invalid ") + pan.getNameMatchResult() + " " + pan.getReferenceId());
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.

The key is read from the STAMPMITRA_API_KEY environment variable unless you call apiKey(...). client.environment() is "test" or "live". Keys that do not match ^sk_(test|live)_[A-Za-z0-9_-]{8,}$ are rejected unless you call allowCustomKey(true). baseUrl must be https:// (plain http:// only for localhost, 127.0.0.1 and ::1, for example a mock server) and must not contain credentials.

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
Builder options
StampMitra client = StampMitra.builder()
    .apiKey(apiKey)
    .baseUrl("https://api-b2b.stampmitra.in")
    .timeout(Duration.ofSeconds(30))      // per attempt
    .maxRetries(2)
    .appInfo("my-app", "1.0.0", "https://example.com")
    .logger(line -> System.err.println("[stampmitra] " + line))
    .validateInput(true)
    .allowCustomKey(false)
    .build();

// Also available: transport, defaultHeader, debug, executor (for the async methods), sleeper (tests).
// Every method also accepts per-request RequestOptions:
// timeout, maxRetries, idempotencyKey, header(name, value)
Resources

Every resource in one place

Orders are identified by the externalOrderId that create returns (there is no id). Methods that take a request class also accept a plain Map<String, Object> for fields the SDK does not model yet. Endpoints whose shape the API does not fix (e-Stamp reference data, duty, parties, webhook events) return GenericResponse, and GET /v1/services returns an iterable ServiceList. Response toString() masks personal data.

All resources
// Verification (POST /v1/verify/*). Every call, including valid=false results, is billed.
client.verify().pan("ABCDE1234F", "Rahul Kumar");
client.verify().gstin("27AABCT1332L1ZZ");
client.verify().rc("MH12AB1234");
client.verify().voterId("ABC1234567");
client.verify().drivingLicense("MH1420110012345", "1990-01-31");   // IP-allowlisted key required
client.verify().passport("PA1079341954215", "1990-01-31");         // IP-allowlisted key required
client.verify().cin("U74999DL2015PTC284009");
client.verify().udyam("UDYAM-MH-12-1234567");
client.verify().panToGstin("ABCDE1234F");
client.verify().panToUdyam("ABCDE1234F");
client.verify().bankAccount("000123456789", "ICIC0001234", "Rahul Kumar", null);

// Services and e-Stamp reference data
client.services().list();
client.services().get("estamp.instrument.rent_agreement");
client.estamp().getSchema("estamp.instrument.rent_agreement");
client.estamp().listStates();
client.estamp().listInstruments("MH");
client.estamp().listArticles("rent_agreement");
client.estamp().calculateDuty(EstampDutyCalculateRequest.builder()
    .stateCode("MH").articleCode("5").stampDutyAmount(new BigDecimal("500")).build());
client.estamp().addParties("o1", EstampPartiesRequest.builder().customerName("A").partyName("B").build());

// Orders and payments (orders are addressed by the externalOrderId you supplied)
client.orders().create(CreateOrderRequest.builder()
    .externalOrderId("o1").externalCustomerId("c1").serviceCode("estamp.instrument.rent_agreement").build());
client.orders().get("o1");
client.orders().getStatus("o1");
client.orders().getPayment("o1");
client.orders().listDocuments("o1");
client.orders().getDocumentDownload("o1", "doc_1");   // short-lived signed URL
client.payments().intents().create(CreatePaymentIntentRequest.builder()
    .externalOrderId("o1").amountMinor(50000).currency("INR").paymentConnectionId("pc_1").build());
client.payments().intents().get("pi_1");

// Usage, logs, webhook deliveries
client.usage().summary();
client.logs().list(LogsParams.builder().service("pan.verify").status("success").limit(50).build());
client.webhooks().events().list(20, null);
client.webhooks().events().replay("evt_1");

// Every method has an Async twin returning a CompletableFuture
CompletableFuture<PanVerification> pan = client.verify().panAsync("ABCDE1234F");
Pagination

Pagination

iterate returns an Iterable that follows nextCursor lazily, stream returns a java.util.stream.Stream and pages iterates whole pages; each call starts again from the first page. list returns a single page.

Java
LogsParams params = LogsParams.builder().service("pan.verify").limit(100).build();

for (LogEntry entry : client.logs().iterate(params)) {
    System.out.println(entry.getId() + " " + entry.getCreatedAt());
}

LogPage page = client.logs().list(params);   // page.getItems(), page.getNextCursor()
long firstFive = client.logs().stream(params).limit(5).count();
Errors

Error classes

All errors are unchecked and extend in.stampmitra.sdk.errors.StampMitraException, which exposes getStatus(), getErrorCode(), getMessage(), getRequestId(), getField(), getBody() and getHeaders(). RateLimitException adds getRetryAfterSeconds() (null when the header is absent). Other statuses, such as an unexpected 3xx, raise the base StampMitraException, and non-JSON error bodies still map by status. Exceptions never contain the API key.

HTTP statusClassNotes
400 / local validationValidationExceptionBad input. Also raised locally, before any request, by the validation helpers.
401AuthenticationExceptionMissing, malformed or revoked API key.
402PaymentRequiredExceptionInsufficient wallet balance or credit.
403PermissionExceptionThe server message is passed through verbatim, e.g. Passport and Driving Licence need an IP-allowlisted key.
404NotFoundExceptionUnknown order, service or event id.
409ConflictExceptionIdempotency-Key reused with a different body, or a state conflict.
429RateLimitExceptionBack off and retry; the server's Retry-After value is exposed when it is sent.
5xxUpstreamExceptionA server-side or upstream failure. Retried automatically for 502, 503 and 504.
network / timeoutConnectionException / RequestTimeoutExceptionNo usable response: connection failure, timeout or cancellation.
webhook verificationSignatureVerificationExceptionBad or stale webhook signature, an empty secret, or a malformed timestamp. Reject the delivery with a 400.
Java
try {
    client.verify().gstin("27AABCT1332L1ZZ");
} catch (RateLimitException e) {
    // back off: e.getRetryAfterSeconds() may be null
} catch (StampMitraException e) {
    System.err.println(e.getRequestId() + ": " + e.getMessage());
}
Retries and idempotency

Retries and idempotency

Validation

Client-side validation

Inputs are checked before sending (PAN, GSTIN, IFSC, CIN, Udyam, vehicle number, 9 to 18 digit account number, passport file number, driving licence, voter ID, phone, ISO dates, names up to 256 characters), trimmed and upper-cased (the account number is only trimmed). A failure is a local ValidationException (getField() names the input) with no network call and no billing. Separators are not stripped, so MH-12 AB 1234 is rejected, like the API does. The validators are public (Validators.pan(...)) and can be disabled 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.

Webhook.constructEvent verifies the signature over the body exactly as received, compares in constant time, rejects timestamps more than 300 s (configurable) from now and rejects an empty secret. Failures raise SignatureVerificationException. Webhook.verifySignature returns a boolean, and Webhook.constructEventFromHeaders reads the three headers from any Map<String, ? extends Collection<String>> such as com.sun.net.httpserver.Headers or a servlet header map.

Java
import in.stampmitra.sdk.Webhook;
import in.stampmitra.sdk.models.WebhookEvent;
import in.stampmitra.sdk.models.WebhookEventType;

// rawBody: the request body as received (do not parse and re-serialise it)
WebhookEvent event = Webhook.constructEvent(rawBody, timestamp, signature, secret);
if (event.getType() == WebhookEventType.ORDER_PAYMENT_CONFIRMED) {
    System.out.println(event.getCoreOrderId() + " paid at " + event.getOccurredAt());
}
boolean authentic = Webhook.verifySignature(rawBody, timestamp, signature, secret);   // tolerance 300 s

WebhookEvent is typed for the eight order.* events (getType(), getCoreOrderId(), getCoreEventType(), getOccurredAt(), getEventId()); the event's own extra fields are preserved (event.get("..."), event.getRaw()). Deliveries are retried, so de-duplicate on the event id from X-Webhook-Event-Id.

Framework recipes

Framework recipes
import in.stampmitra.sdk.Webhook;
import in.stampmitra.sdk.errors.SignatureVerificationException;
import in.stampmitra.sdk.models.WebhookEvent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

// Take the body as byte[]: the signature covers the raw bytes, so never let Spring parse the JSON first.
@RestController
public class WebhookController {

    private final String secret;

    public WebhookController(@Value("${stampmitra.webhook-secret}") String secret) {
        this.secret = secret;
    }

    @PostMapping("/webhooks/stampmitra")
    public ResponseEntity<String> receive(
            @RequestBody byte[] rawBody,
            @RequestHeader(Webhook.HEADER_TIMESTAMP) String timestamp,
            @RequestHeader(Webhook.HEADER_SIGNATURE) String signature,
            @RequestHeader(value = Webhook.HEADER_EVENT_ID, required = false) String eventId) {
        try {
            WebhookEvent event = Webhook.constructEvent(rawBody, timestamp, signature, secret,
                    Webhook.DEFAULT_TOLERANCE_SECONDS, eventId, System.currentTimeMillis() / 1000L);
            System.out.println("verified " + event.getType() + " order=" + event.getCoreOrderId());
            return ResponseEntity.ok("ok");
        } catch (SignatureVerificationException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("invalid signature: " + e.getErrorCode());
        }
    }
}

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

Safe logging

Safe logging

Nothing is logged by default. Pass logger(line -> ...) or debug(true) (writes to the System.Logger named in.stampmitra.sdk at DEBUG) to get one line per request, response and retry. Authorization and x-api-key are redacted, identifiers keep their last four characters and names, dates of birth and addresses are removed. The API key never appears in log lines, exception messages, stack traces or toString() output.

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.