StampMitraDevelopers
Official SDK

PHP SDK

The official PHP client for the StampMitra Developer API: identity verification and e-Stamp with typed immutable models, PSR-18 transport and signed-webhook helpers for Laravel and plain PHP.

Pre-release 0.1.0 - package registry publishing in progress

Package
stampmitra/stampmitra-php
Version
0.1.0
Requires
PHP 8.1 or newer. The SDK talks PSR-18 and PSR-17, so any compatible HTTP client works: Guzzle is auto-discovered, or pass your own httpClient.
License
MIT
Changelog
CHANGELOG.md
Install

Install the package

Composer
composer require stampmitra/stampmitra-php guzzlehttp/guzzle

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

Composer (from GitHub)
composer config repositories.stampmitra vcs https://github.com/Bani-Global-Industries-LLP/stampmitra-developer-php
composer require stampmitra/stampmitra-php:dev-main guzzlehttp/guzzle

Until then, install from GitHub: composer.json sits at the repository root with the package name stampmitra/stampmitra-php, so Composer can read it as a VCS repository.

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.

PHP
use StampMitra\StampMitra;

$client = new StampMitra(apiKey: getenv('STAMPMITRA_API_KEY') ?: null); // or just new StampMitra()
$pan = $client->verify->pan('ABCDE1234F', 'Rahul Kumar');
echo $pan->valid ? 'valid' : 'invalid', ' ', $pan->nameMatchResult, ' ', $pan->referenceId, PHP_EOL;
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 pass apiKey:. $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.

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 = new StampMitra(
    apiKey: 'sk_test_...',
    baseUrl: 'https://api-b2b.stampmitra.in',
    timeout: 30,
    maxRetries: 2,
    httpClient: null,        // any PSR-18 client; Guzzle is auto-discovered
    defaultHeaders: [],
    appInfo: ['name' => 'my-app', 'version' => '1.0.0', 'url' => 'https://example.com'],
    logger: null,            // any PSR-3 logger
    validateInput: true,
    allowCustomKey: false,
);

// Every method also takes per-request options (a RequestOptions or an array):
$client->orders->create($body, ['idempotencyKey' => 'order-123']);
Resources

Every resource in one place

Responses are typed, immutable models ($pan->registeredName). Fields the SDK does not know about are never dropped: $pan->raw() returns the full decoded body and $pan->get('field') reads any field. HTTP metadata (status, headers, request id, attempts) is on $pan->response(). Endpoints whose schema is not fixed yet return GenericResponse.

All resources
// Verification (every call is billed, including valid=false results)
$client->verify->pan('ABCDE1234F', 'Rahul Kumar');
$client->verify->gstin('27ABCDE1234F1Z5');
$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', name: 'Rahul Kumar');

// 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(['stateCode' => 'MH', 'articleCode' => '5']);
$client->estamp->addParties('ord_123', ['customerName' => '...', 'partyName' => '...']);

// Orders and payments
$order = $client->orders->create(['externalOrderId' => 'o1', 'externalCustomerId' => 'c1', 'serviceCode' => 'estamp....']);
$client->orders->get('o1');
$client->orders->getStatus('o1');
$client->orders->getPayment('o1');
$client->orders->listDocuments('o1');
$client->orders->getDocumentDownload('o1', 'doc_1');
$client->payments->intents->create(['externalOrderId' => 'o1', 'amountMinor' => 50000, 'currency' => 'INR', 'paymentConnectionId' => '...']);
$client->payments->intents->get('pi_1');

// Usage, logs, webhook events
$client->usage->summary();
$client->logs->list(['service' => 'pan.verify', 'status' => 'success', 'limit' => 50]);
$client->webhooks->events->list(limit: 20);
$client->webhooks->events->replay('evt_1');
Pagination

Pagination

iterate() returns a lazy Generator that follows nextCursor; list() returns a single page.

Pagination
foreach ($client->logs->iterate(['service' => 'pan.verify', 'limit' => 100]) as $entry) {
    echo $entry->id, ' ', $entry->createdAt, PHP_EOL;   // follows nextCursor lazily (a Generator)
}

$page = $client->logs->list(['limit' => 50]);          // $page->items, $page->nextCursor
Errors

Error classes

All errors live in StampMitra\Exception and extend StampMitraError (getStatus(), getErrorCode(), getMessage(), getRequestId(), getField(), getBody()). RateLimitError adds getRetryAfter() when the header is present. Other statuses raise the base StampMitraError, and non-JSON error bodies still map by status.

HTTP statusClassNotes
400 (and local validation)ValidationErrorBad 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 / timeoutConnectionError / TimeoutErrorNo usable response: connection failure, timeout or cancellation.
PHP
try {
    $client->verify->gstin('27ABCDE1234F1Z5');
} catch (\StampMitra\Exception\RateLimitError $e) {
    // back off: $e->getRetryAfter()
} catch (\StampMitra\Exception\StampMitraError $e) {
    error_log($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) and normalised to upper case. A failure is a local ValidationError 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.

Webhook::constructEvent verifies the signature over the body exactly as received, compares in constant time and rejects timestamps more than 300 s (configurable) from now. Failures raise SignatureVerificationError. Webhook::verifySignature returns a bool and Webhook::constructEventFromHeaders reads the headers for you.

PHP
use StampMitra\Webhook;

$payload = file_get_contents('php://input');           // raw bytes, do not re-encode
$event = Webhook::constructEvent(
    $payload,
    $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'],
    $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'],
    $secret,
);
// or: Webhook::verifySignature($payload, $timestamp, $signature, $secret, toleranceSeconds: 300): bool
// or: Webhook::constructEventFromHeaders($payload, $headers, $secret)

Deliveries are retried, so handle events idempotently and de-duplicate on the event id from X-Webhook-Event-Id.

Framework recipes

Framework recipes
<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use StampMitra\Exception\SignatureVerificationError;
use StampMitra\Webhook;

$secret = getenv('STAMPMITRA_WEBHOOK_SECRET') ?: '';

// The signature covers the raw bytes: read the body untouched, never re-encode it.
$payload = (string) file_get_contents('php://input');

try {
    $event = Webhook::constructEvent(
        $payload,
        $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '',
        $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
        $secret,
        eventId: $_SERVER['HTTP_X_WEBHOOK_EVENT_ID'] ?? null,
    );
} catch (SignatureVerificationError $e) {
    http_response_code(400);
    echo 'invalid signature';
    exit;
}

// Handle idempotently: deliveries are retried, so de-duplicate on $event->eventId.
http_response_code(200);
echo 'ok';

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

Safe logging

Safe logging

Nothing is logged by default. Pass any PSR-3 logger as logger: to get debug logs of requests and responses, with Authorization and x-api-key redacted and identifiers and names in bodies 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.