StampMitraDevelopers
Official SDK

.NET SDK

The official .NET client for the StampMitra Developer API: async-first identity verification and e-Stamp with typed records, IAsyncEnumerable pagination, automatic retries, signed-webhook helpers and ASP.NET Core dependency injection.

Pre-release 0.1.0 - package registry publishing in progress

Package
StampMitra
Version
0.1.0
Requires
net6.0 or net8.0 (and therefore every later runtime). There is no netstandard2.1 or .NET Framework build. The core package uses only System.Net.Http.HttpClient and System.Text.Json, with no third-party runtime dependencies. C# 9 or newer is recommended (records and init-only properties). .NET 6 is past its end of support and is kept as a target for existing applications.
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

.NET CLI
dotnet add package StampMitra
# optional: services.AddStampMitra(...) for ASP.NET Core / generic host
dotnet add package StampMitra.Extensions.DependencyInjection

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

C#
using StampMitra;
using StampMitra.Models;

using var client = new StampMitraClient();   // reads STAMPMITRA_API_KEY
var pan = await client.Verify.PanAsync(new PanVerifyRequest { Pan = "ABCDE1234F", Name = "Rahul Kumar" }, ct);
Console.WriteLine($"{(pan.Valid == true ? "valid" : "invalid")} {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.

The client reads STAMPMITRA_API_KEY unless you set StampMitraClientOptions.ApiKey. Keys that do not match ^sk_(test|live)_[A-Za-z0-9_-]{8,}$ are rejected unless AllowCustomKey is set. client.Environment is Test, Live or Custom. BaseUrl must be https (loopback http only, for mock servers) with no credentials in the URL. The API key never appears in ToString(), exceptions or logs. ct in the samples is a CancellationToken; use CancellationToken.None if you do not need cancellation.

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
using var client = new StampMitraClient(new StampMitraClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("STAMPMITRA_API_KEY"),
    BaseUrl = "https://api-b2b.stampmitra.in",
    Timeout = TimeSpan.FromSeconds(20),   // per attempt, default 30 s
    MaxRetries = 3,                       // default 2, range 0 to 10
    AppInfo = new AppInfo("my-app", "1.4.0", "https://example.com"),
    DebugLog = line => Console.Error.WriteLine(line),
    ValidateInput = true,
    AllowCustomKey = false,
});

// Also available: HttpClient (yours is never disposed), DefaultHeaders.
// Every method also takes a per-call RequestOptions (Timeout, MaxRetries, IdempotencyKey, Headers) and a CancellationToken.
Resources

Every resource in one place

Responses are immutable records (pan.RegisteredName). Fields the SDK does not know are kept in AdditionalProperties (name to JsonElement), and HTTP metadata (status, headers, request id, attempts) is on model.Response. Responses whose shape the API does not fix (e-Stamp reference data, duty, parties, webhook events) are GenericResponse (Data, Get(name), Items, NextCursor, RawJson), and GET /v1/services returns a bare array exposed as ServiceList.

All resources
using StampMitra.Models;      // models and request types
using StampMitra.Resources;   // LogsListParams

// Verification: every call is billed, including valid = false results
var pan = await client.Verify.PanAsync(new PanVerifyRequest { Pan = "ABCDE1234F", Name = "Rahul Kumar" }, ct);
var gst = await client.Verify.GstinAsync(new GstinVerifyRequest { Gstin = "27ABCDE1234F1Z5" }, ct);
var rc = await client.Verify.RcAsync(new VehicleRcVerifyRequest { VehicleNumber = "MH12AB1234" }, ct);
var bank = await client.Verify.BankAccountAsync(new BankAccountVerifyRequest { AccountNumber = "000123456789", Ifsc = "ICIC0001234", Name = "Rahul Kumar" }, ct);
// Also: VoterIdAsync, DrivingLicenseAsync* , PassportAsync* , CinAsync, UdyamAsync, PanToGstinAsync, PanToUdyamAsync
// * needs an IP-allowlisted key; otherwise PermissionDeniedException carries the server message

// Services and e-Stamp reference data
ServiceList services = await client.Services.ListAsync(ct);
GenericResponse states = await client.Estamp.ListStatesAsync(ct);
GenericResponse duty = await client.Estamp.CalculateDutyAsync(new EstampDutyCalculateRequest { StateCode = "MH", ArticleCode = "5", StampDutyAmount = 500 }, ct);
// Also: Services.GetAsync, Estamp.GetSchemaAsync, ListInstrumentsAsync(stateCode), ListArticlesAsync(instrumentCode), AddPartiesAsync(orderId, ...)

// Orders and payments (addressed by the externalOrderId you supplied)
var order = await client.Orders.CreateAsync(
    new CreateOrderRequest { ExternalOrderId = "o1", ExternalCustomerId = "c1", ServiceCode = "estamp.instrument.rent_agreement" },
    new RequestOptions { IdempotencyKey = "order-o1", Timeout = TimeSpan.FromSeconds(10) },
    ct);
OrderStatusResponse status = await client.Orders.GetStatusAsync(order.ExternalOrderId!, ct);
// Also: Orders.GetAsync, GetPaymentAsync, ListDocumentsAsync, GetDocumentDownloadAsync(id, documentId), Payments.Intents.CreateAsync / GetAsync

// Usage, logs, webhook deliveries
UsageSummary usage = await client.Usage.SummaryAsync(ct);
LogsPage logs = await client.Logs.ListAsync(new LogsListParams { Status = "failure", Limit = 50 }, ct);
GenericResponse events = await client.Webhooks.Events.ListAsync(20, null, null, ct);
// Also: Webhooks.Events.ReplayAsync
Pagination

Pagination

ListAllAsync returns an IAsyncEnumerable<LogEntry> that follows nextCursor lazily; ListAsync returns a single page with Items and NextCursor.

C#
await foreach (var entry in client.Logs.ListAllAsync(new LogsListParams { Service = "pan.verify", Limit = 100 }, null, ct))
{
    Console.WriteLine($"{entry.Id} {entry.CreatedAt}");   // follows nextCursor lazily
}

LogsPage page = await client.Logs.ListAsync(new LogsListParams { Status = "failure", Limit = 50 }, ct);   // page.Items, page.NextCursor
Errors

Error classes

All errors derive from StampMitraException, which carries Status, Code, Message, RequestId, Field and Body. RateLimitException adds RetryAfter when the header is present. Any other status raises the base StampMitraException, and non-JSON error bodies still map by status. Caller cancellation surfaces as OperationCanceledException, never wrapped.

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.
403PermissionDeniedExceptionThe 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 / timeoutApiConnectionException / ApiTimeoutExceptionNo 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.
C#
try
{
    await client.Verify.GstinAsync(new GstinVerifyRequest { Gstin = "27ABCDE1234F1Z5" }, ct);
}
catch (RateLimitException ex)
{
    Console.WriteLine($"back off {ex.RetryAfter}");
}
catch (StampMitraException ex)
{
    Console.Error.WriteLine($"{ex.RequestId}: {ex.Message}");
}
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), trimmed and upper-cased where the API does so. A failure is a local ValidationException (Field is set) with no network call and no billing. Separators such as MH-12 AB 1234 are not stripped, exactly like the API. 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.

WebhookSignature.ConstructEvent returns a typed record for each of the eight events (Type is the documented name such as order.created) or an UnknownWebhookEvent; extra fields are kept in AdditionalProperties. The comparison is constant-time, timestamps further than 300 s (configurable) from now are rejected, an empty secret is rejected, and failures raise SignatureVerificationException (Code is SIGNATURE_MISMATCH, INVALID_TIMESTAMP, TIMESTAMP_OUTSIDE_TOLERANCE, INVALID_PAYLOAD, MISSING_HEADERS or MISSING_SECRET). WebhookSignature.Verify returns a bool, and ConstructEventFromHeaders reads the three headers through a lookup.

C#
using StampMitra.Webhooks;

// rawBody: the request body bytes, untouched (never parse and re-serialise)
WebhookEvent evt = WebhookSignature.ConstructEvent(rawBody, timestamp, signature, secret, eventId: eventId);

switch (evt)
{
    case OrderPaymentConfirmedEvent paid:
        Console.WriteLine($"paid: {paid.CoreOrderId}");
        break;
    case OrderDocumentReadyEvent ready:
        Console.WriteLine($"document ready: {ready.CoreOrderId}");
        break;
}

bool authentic = WebhookSignature.Verify(rawBody, timestamp, signature, secret, toleranceSeconds: 300);

Deliveries are retried by the platform, so de-duplicate on EventId (from X-Webhook-Event-Id) before doing any work.

Framework recipes

Framework recipes
using StampMitra;
using StampMitra.Webhooks;

var secret = Environment.GetEnvironmentVariable("STAMPMITRA_WEBHOOK_SECRET");
if (string.IsNullOrEmpty(secret))
{
    Console.Error.WriteLine("Set STAMPMITRA_WEBHOOK_SECRET to the endpoint's signing secret.");
    return 1;
}

var app = WebApplication.CreateBuilder(args).Build();

app.MapPost("/webhooks/stampmitra", async (HttpRequest request, ILogger<Program> logger) =>
{
    // The signature covers the raw request bytes: read the body untouched, never re-serialise it.
    using var ms = new MemoryStream();
    await request.Body.CopyToAsync(ms);
    var raw = ms.ToArray();

    WebhookEvent evt;
    try
    {
        evt = WebhookSignature.ConstructEventFromHeaders(raw, name => request.Headers[name].FirstOrDefault(), secret);
    }
    catch (SignatureVerificationException ex)
    {
        logger.LogWarning("Rejected webhook: {Code}", ex.Code);
        return Results.BadRequest(new { error = "invalid signature" });
    }

    // Deliveries are retried: de-duplicate on evt.EventId before doing any work.
    switch (evt)
    {
        case OrderPaymentConfirmedEvent paid:
            logger.LogInformation("Payment confirmed for order {Order}", paid.CoreOrderId);
            break;
        case OrderDocumentReadyEvent ready:
            logger.LogInformation("Document ready for order {Order}", ready.CoreOrderId);
            break;
        default:
            logger.LogInformation("{Type} for order {Order} (event {EventId})", evt.Type ?? evt.CoreEventType, evt.CoreOrderId, evt.EventId);
            break;
    }

    return Results.Ok(new { received = true });
});

await app.RunAsync();
return 0;

// Makes ILogger<Program> resolvable in a top-level-statements program.
public partial class Program
{
}

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

Safe logging

Safe logging

Nothing is logged by default. Set DebugLog (an Action<string>) to receive one line per request and response. Authorization and x-api-key are redacted, identifiers (PAN, GSTIN, account number and so on) are masked to their last four characters and names and dates of birth are removed; Redactor is public if you need the same masking elsewhere. With the DI package the lines go to ILogger at Debug level. Treat logs as sensitive anyway.

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.