StampMitraDevelopers
Official SDK

Go SDK

The official Go client for the StampMitra Developer API: identity verification and e-Stamp with context-aware calls, typed responses, errors.Is-friendly errors, automatic retries and signed-webhook helpers, using only the standard library.

Pre-release 0.1.0 - package registry publishing in progress

Package
github.com/Bani-Global-Industries-LLP/stampmitra-developer-go
Version
0.1.0
Requires
Go 1.21 or newer (CI runs 1.21 to 1.24). The standard library only: no third-party dependencies. The package name is stampmitra, and the module path is case-sensitive, so import it with the capital letters exactly as shown.
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

go get
go get github.com/Bani-Global-Industries-LLP/[email protected]

Available once published. Not yet published to the Go module proxy. 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.

Go
import stampmitra "github.com/Bani-Global-Industries-LLP/stampmitra-developer-go"

client, err := stampmitra.NewClient("") // empty = read STAMPMITRA_API_KEY
if err != nil { log.Fatal(err) }
pan, err := client.Verify.PAN(context.Background(), &stampmitra.PANVerifyParams{PAN: "ABCDE1234F", Name: "Rahul Kumar"})
if err != nil { log.Fatal(err) }
fmt.Println(pan.Valid, pan.NameMatchResult, pan.ReferenceID)
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.

NewClient("") reads the STAMPMITRA_API_KEY environment variable. Keys that do not match ^sk_(test|live)_[A-Za-z0-9_-]{8,}$ are rejected unless you pass WithAllowCustomKey(). client.Environment is "test", "live" or "custom". The base URL must be https (plain http only for localhost, 127.0.0.1 and ::1), with no credentials and no query. The API key never appears in fmt output of the client (%v, %+v, %#v), errors or logs.

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
client, err := stampmitra.NewClient(key,
    stampmitra.WithBaseURL("https://api-b2b.stampmitra.in"),
    stampmitra.WithTimeout(30*time.Second),            // per attempt
    stampmitra.WithMaxRetries(2),
    stampmitra.WithBackoff(500*time.Millisecond, 8*time.Second),
    stampmitra.WithHTTPClient(httpClient),             // redirects are still never followed
    stampmitra.WithDefaultHeader("X-Trace", "abc"),
    stampmitra.WithAppInfo(stampmitra.AppInfo{Name: "my-app", Version: "1.0.0"}),
    stampmitra.WithLogger(slog.Default()),
    stampmitra.WithValidateInput(true),
    stampmitra.WithAllowCustomKey(),
)

// Every method takes a context.Context first and per-call options last:
// WithRequestTimeout, WithRequestMaxRetries, WithIdempotencyKey, WithRequestHeader, WithRawResponse
Resources

Every resource in one place

Responses are structs with json tags (pan.RegisteredName). Every response struct has an Extra map[string]json.RawMessage for fields this SDK version does not model, and json.Marshal writes them back. Endpoints whose shape the API does not fix (e-Stamp reference data, duty, parties, webhook event lists and replays) return *GenericResponse (Raw, Decode, Get, Map, Items, NextCursor). Orders are addressed by the externalOrderId that Create returns (there is no id).

All resources
ctx := context.Background()

// Verification (every call, including valid=false results, is billed)
pan, err := client.Verify.PAN(ctx, &stampmitra.PANVerifyParams{PAN: "ABCDE1234F", Name: "Rahul Kumar"})
gst, err := client.Verify.GSTIN(ctx, &stampmitra.GSTINVerifyParams{GSTIN: "27ABCDE1234F1Z5"})
rc, err := client.Verify.RC(ctx, &stampmitra.RCVerifyParams{VehicleNumber: "MH12AB1234"})
voter, err := client.Verify.VoterID(ctx, &stampmitra.VoterIDVerifyParams{VoterID: "ABC1234567"})
dl, err := client.Verify.DrivingLicense(ctx, &stampmitra.DrivingLicenseVerifyParams{DLNumber: "MH1420110012345", DOB: "1990-01-31"}) // IP-allowlisted key
pp, err := client.Verify.Passport(ctx, &stampmitra.PassportVerifyParams{FileNumber: "PA1079341954215", DOB: "1990-01-31"})        // IP-allowlisted key
cin, err := client.Verify.CIN(ctx, &stampmitra.CINVerifyParams{CIN: "U74999DL2015PTC284009"})
udyam, err := client.Verify.Udyam(ctx, &stampmitra.UdyamVerifyParams{UdyamNumber: "UDYAM-MH-12-1234567"})
gstins, err := client.Verify.PANToGSTIN(ctx, &stampmitra.PANToGSTINParams{PAN: "ABCDE1234F"})
udyams, err := client.Verify.PANToUdyam(ctx, &stampmitra.PANToUdyamParams{PAN: "ABCDE1234F"})
bank, err := client.Verify.BankAccount(ctx, &stampmitra.BankAccountVerifyParams{AccountNumber: "000123456789", IFSC: "ICIC0001234", Name: "Rahul Kumar"})

// Services and e-Stamp reference data
services, err := client.Services.List(ctx)
service, err := client.Services.Get(ctx, "estamp.instrument.rent_agreement")
schema, err := client.Estamp.GetSchema(ctx, "estamp.instrument.rent_agreement")
states, err := client.Estamp.ListStates(ctx)
instruments, err := client.Estamp.ListInstruments(ctx, "MH")
articles, err := client.Estamp.ListArticles(ctx, "rent_agreement")
duty, err := client.Estamp.CalculateDuty(ctx, &stampmitra.EstampDutyParams{StateCode: "MH", ArticleCode: "5", StampDutyAmount: 500})
parties, err := client.Estamp.AddParties(ctx, "ord_ext_123", &stampmitra.EstampPartiesParams{CustomerName: "A", PartyName: "B"})

// Orders and payments: orders are addressed by YOUR externalOrderId
order, err := client.Orders.Create(ctx, &stampmitra.CreateOrderParams{
    ExternalOrderID: "ord_ext_123", ExternalCustomerID: "cust_ext_456", ServiceCode: "estamp.instrument.rent_agreement",
}, stampmitra.WithIdempotencyKey("create-ord_ext_123"))
detail, err := client.Orders.Get(ctx, order.ExternalOrderID)
status, err := client.Orders.GetStatus(ctx, order.ExternalOrderID)
payment, err := client.Orders.GetPayment(ctx, order.ExternalOrderID)
docs, err := client.Orders.ListDocuments(ctx, order.ExternalOrderID)
link, err := client.Orders.GetDocumentDownload(ctx, order.ExternalOrderID, "doc_1") // link.DownloadURL is short-lived
intent, err := client.Payments.Intents.Create(ctx, &stampmitra.CreatePaymentIntentParams{ExternalOrderID: "ord_ext_123", AmountMinor: 50000, Currency: "INR", PaymentConnectionID: "pc_1"})
intent, err = client.Payments.Intents.Get(ctx, intent.ID)

// Usage, logs, webhook deliveries
usage, err := client.Usage.Summary(ctx)
page, err := client.Logs.List(ctx, &stampmitra.LogsListParams{Service: "pan.verify", Status: "success", Limit: 50})
events, err := client.Webhooks.Events.List(ctx, &stampmitra.WebhookEventsListParams{Limit: 20})
replay, err := client.Webhooks.Events.Replay(ctx, "evt_1")

// Raw HTTP metadata
var raw stampmitra.Response
st, err := client.Orders.GetStatus(ctx, "ord_ext_123", stampmitra.WithRawResponse(&raw))
fmt.Println(st != nil, err == nil, raw.StatusCode, raw.RequestID, raw.Attempts)
Pagination

Pagination

ListAll performs no I/O itself; Next fetches pages lazily and follows nextCursor. For one page use Logs.List. Webhooks.Events.List returns a *GenericResponse with Items() and NextCursor().

Go
it := client.Logs.ListAll(&stampmitra.LogsListParams{Service: "pan.verify", Limit: 100})
for it.Next(ctx) {
    entry := it.Log()
    fmt.Println(entry.ID, entry.CreatedAt)
}
if err := it.Err(); err != nil {
    log.Fatal(err)
}

page, err := client.Logs.List(ctx, &stampmitra.LogsListParams{Limit: 50}) // page.Items, page.NextCursor
Errors

Error classes

Every non-2xx response is an *stampmitra.APIError with Status, Code, Message, RequestID, Field, RetryAfter, Body and Attempts. Classify it with errors.Is against the sentinels, or with the Is* helpers. Other statuses are still an *APIError without a sentinel, and non-JSON error bodies map by status. A cancelled or expired context returns the context's own error, so errors.Is(err, context.Canceled) works.

HTTP statusClassNotes
400 / local validationErrValidation / IsValidationBad input. Also raised locally, before any request, by the validation helpers.
401ErrAuthentication / IsAuthenticationMissing, malformed or revoked API key.
402ErrPaymentRequired / IsPaymentRequiredInsufficient wallet balance or credit.
403ErrPermission / IsPermissionThe server message is passed through verbatim, e.g. Passport and Driving Licence need an IP-allowlisted key.
404ErrNotFound / IsNotFoundUnknown order, service or event id.
409ErrConflict / IsConflictIdempotency-Key reused with a different body, or a state conflict.
429ErrRateLimit / IsRateLimitBack off and retry; the server's Retry-After value is exposed when it is sent.
5xxErrUpstream / IsUpstreamA server-side or upstream failure. Retried automatically for 502, 503 and 504.
network / timeout*ConnectionError (ErrConnection, ErrTimeout) / IsConnection, IsTimeoutNo usable response: connection failure, timeout or cancellation.
webhook verification*SignatureVerificationErrorBad or stale webhook signature, an empty secret, or a malformed timestamp. Reject the delivery with a 400.
Go
_, err := client.Verify.GSTIN(ctx, &stampmitra.GSTINVerifyParams{GSTIN: "27ABCDE1234F1Z5"})
var apiErr *stampmitra.APIError
switch {
case err == nil:
    // success
case errors.As(err, &apiErr) && stampmitra.IsRateLimit(err):
    log.Println("rate limited; retry after", apiErr.RetryAfter) // zero when the API sent no Retry-After
case errors.As(err, &apiErr):
    log.Printf("%d %s (request %s): %s", apiErr.Status, apiErr.Code, apiErr.RequestID, apiErr.Message)
case stampmitra.IsValidation(err):
    log.Println("invalid input, nothing was sent:", err)
default:
    log.Println("request failed:", err)
}
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, length limits). Values are trimmed and ASCII upper-cased (bank account numbers are only trimmed), and spaces or hyphens inside identifiers are rejected, never silently removed. A failure returns a *ValidationError (it matches ErrValidation) with no network call and no billing, and its message never echoes the value. The validators are exported (ValidatePAN, ValidateGSTIN, ValidateIFSC and so on); turn all of it off with WithValidateInput(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.

Verification lives in the main package: ConstructEvent, ConstructEventFromHeaders and VerifySignature. The comparison is constant-time, timestamps more than 300 s (WithTolerance) from now are rejected, an empty secret is rejected, and failures return *SignatureVerificationError (Reason: invalid_timestamp, timestamp_outside_tolerance, signature_mismatch and others), which never contains the secret. SignPayload computes a signature for tests.

Go
// body: raw bytes from io.ReadAll(r.Body); do not decode and re-encode
ev, err := stampmitra.ConstructEventFromHeaders(body, r.Header, secret)
if err != nil {
    var sigErr *stampmitra.SignatureVerificationError
    if errors.As(err, &sigErr) {
        log.Println("rejected:", sigErr.Reason)
    }
    return
}
switch e := ev.(type) {
case *stampmitra.OrderDocumentReadyEvent:
    log.Println("document ready for", e.CoreOrderID, e.ID)
case *stampmitra.UnknownEvent:
    log.Println("unrecognised event", e.RawType)
}

The eight order.* events decode to typed values (*OrderCreatedEvent, *OrderPaymentConfirmedEvent, *OrderFulfilmentStartedEvent, *OrderFulfilmentCompletedEvent, *OrderFailedEvent, *OrderCancelledEvent, *OrderRefundCompletedEvent, *OrderDocumentReadyEvent), all embedding OrderEvent (CoreOrderID, CoreEventType, OccurredAt, ID and Extra). Anything else is an *UnknownEvent. Deliveries are retried, so de-duplicate on the event id from X-Webhook-Event-Id.

Framework recipes

Framework recipes
package main

import (
    "errors"
    "io"
    "log"
    "net/http"
    "os"

    stampmitra "github.com/Bani-Global-Industries-LLP/stampmitra-developer-go"
)

const maxBody = 1 << 20 // 1 MiB

func handler(secret string) http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/webhooks/stampmitra", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }
        // Raw bytes: the signature covers the body exactly as received.
        body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBody))
        if err != nil {
            http.Error(w, "cannot read body", http.StatusBadRequest)
            return
        }
        ev, err := stampmitra.ConstructEventFromHeaders(body, r.Header, secret)
        if err != nil {
            var sigErr *stampmitra.SignatureVerificationError
            if errors.As(err, &sigErr) {
                log.Printf("rejected webhook: %s", sigErr.Reason) // never log the secret or signature
            }
            http.Error(w, "invalid signature", http.StatusBadRequest)
            return
        }

        switch e := ev.(type) {
        case *stampmitra.OrderCreatedEvent:
            log.Printf("order created: %s", e.CoreOrderID)
        case *stampmitra.OrderPaymentConfirmedEvent:
            log.Printf("payment confirmed: %s", e.CoreOrderID)
        case *stampmitra.OrderDocumentReadyEvent:
            log.Printf("document ready: %s (event %s)", e.CoreOrderID, e.ID)
        case *stampmitra.UnknownEvent:
            log.Printf("ignoring unrecognised event %q", e.RawType)
        }
        w.WriteHeader(http.StatusNoContent) // any 2xx acknowledges; a non-2xx is retried
    })
    return mux
}

func main() {
    log.Fatal(http.ListenAndServe(":8080", handler(os.Getenv("WEBHOOK_SECRET"))))
}

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

Safe logging

Safe logging

Nothing is logged by default. WithLogger(slog.New(...)) logs requests, responses and retries at slog.LevelDebug. Authorization and X-API-Key are redacted, and identifiers (PAN, GSTIN, account numbers and so on) and personal data (names, dates of birth, addresses) in bodies are 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.