StampMitraDevelopers
इवेंट

Webhooks

ऑर्डर lifecycle के इवेंट आपके endpoint तक signed requests और retries के साथ पहुँचाए जाते हैं।

जल्द आ रहा है

B2B प्लेटफ़ॉर्म ऑर्डर lifecycle इवेंट के लिए outbound webhooks किसी भी ऐसे endpoint पर भेजता है जिसे आपने आंतरिक रूप से रजिस्टर किया हो (organization के WebhookEndpoint record के ज़रिए), HMAC-SHA256 request signing और exponential-backoff retries के साथ (डिलीवरी के dead-letter होने से पहले लगभग 13 घंटों में अधिकतम 6 प्रयास)।

StampMitra का अपना पेमेंट provider अपने webhooks POST /v1/payments/webhooks/:connectionId पर भेजता है, जो server-to-server है, signature से verify होता है, और इस frontend द्वारा नहीं बुलाया जाता।

Retry नीति

Exponential backoff, लगभग 13 घंटों में अधिकतम 6 प्रयास, फिर डिलीवरी dead-letter कर दी जाती है।

डिलीवरी का प्रवाह

ऑर्डर इवेंट से आपके endpoint तक

  1. चरण 1

    ऑर्डर इवेंट

    ऑर्डर पर कुछ बदलता है

  2. चरण 2

    Signed request

    HMAC-SHA256 + timestamp header

  3. चरण 3

    आपका endpoint

    Verify करें, फिर 2xx से जवाब दें

  4. चरण 4

    Retries

    Exponential backoff, अधिकतम 6 प्रयास

Verify

Signature जाँचें

हर डिलीवरी JSON body और तीन header वाली POST रिक्वेस्ट होती है। Signature, hex में एन्कोड किया गया HMAC-SHA256(secret, `${timestamp}.${rawBody}`) है, जहाँ rawBody रिक्वेस्ट की body है, ठीक वैसी जैसी मिली। जाँच raw bytes पर करें: JSON को पहले parse करके दोबारा serialise न करें।

Headerमान
X-Webhook-Signatureआपके endpoint secret से बना, `${timestamp}.${rawBody}` का hex-encoded HMAC-SHA256।
X-Webhook-Timestampइस प्रयास पर signature बनने का Unix समय (सेकंड में)। यह signed string का हिस्सा है।
X-Webhook-Event-Idइवेंट की स्थिर id। विफल डिलीवरी दोबारा भेजी जाती है, इसलिए duplicate पहचानने के लिए इसका उपयोग करें।

प्लेटफ़ॉर्म में timestamp की कोई tolerance तय नहीं है। हमारा सुझाव है कि जिस डिलीवरी का timestamp आपकी घड़ी से 300 सेकंड से ज़्यादा दूर हो, उसे अस्वीकार करें (आधिकारिक SDKs डिफ़ॉल्ट रूप से यही करते हैं)। Signature की तुलना constant-time में करें।

Node.js
import crypto from "node:crypto";

// rawBody: the request body exactly as received (string or Buffer), never re-serialised JSON.
// timestamp / signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
function isValid(secret, timestamp, rawBody, signature) {
  if (!timestamp || !signature) return false; // header missing
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false; // replay guard
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  // constant-time compare (lengths must match first)
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib
import hmac
import time

def is_valid(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    # raw_body: the request body exactly as received (bytes), never re-serialised JSON.
    if not timestamp or not signature:  # header missing
        return False
    try:
        if abs(time.time() - int(timestamp)) > 300:  # replay guard
            return False
    except ValueError:
        return False
    expected = hmac.new(
        secret.encode(),
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    # constant-time compare
    return hmac.compare_digest(expected, signature)
PHP
<?php
// $rawBody: file_get_contents('php://input'), exactly as received, never re-encoded JSON.
// $timestamp / $signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
function isValid(string $secret, string $timestamp, string $rawBody, string $signature): bool
{
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
        return false; // replay guard
    }
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $signature); // constant-time compare
}
Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

// rawBody: the request body exactly as received (bytes), never re-serialised JSON.
// timestamp / signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
static boolean isValid(String secret, String timestamp, byte[] rawBody, String signature) throws Exception {
    long ts;
    try {
        ts = Long.parseLong(timestamp);
    } catch (NumberFormatException e) {
        return false;
    }
    if (Math.abs(System.currentTimeMillis() / 1000 - ts) > 300) return false; // replay guard

    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
    StringBuilder expected = new StringBuilder();
    for (byte b : mac.doFinal(rawBody)) expected.append(String.format("%02x", b));

    // constant-time compare
    return MessageDigest.isEqual(
        expected.toString().getBytes(StandardCharsets.UTF_8),
        signature.getBytes(StandardCharsets.UTF_8));
}
Go
import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"strconv"
	"time"
)

// rawBody: the request body exactly as received (io.ReadAll), never re-encoded JSON.
// timestamp / signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
func isValid(secret, timestamp string, rawBody []byte, signature string) bool {
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}
	if age := time.Now().Unix() - ts; age > 300 || age < -300 { // replay guard
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	// constant-time compare
	return subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) == 1
}
C#
using System.Linq;
using System.Security.Cryptography;
using System.Text;

// rawBody: the request body exactly as received (bytes), never re-serialised JSON.
// timestamp / signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
static bool IsValid(string secret, string timestamp, byte[] rawBody, string signature)
{
    if (!long.TryParse(timestamp, out var ts)) return false;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false; // replay guard

    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var signed = Encoding.UTF8.GetBytes(timestamp + ".").Concat(rawBody).ToArray();
    var expected = Convert.ToHexString(hmac.ComputeHash(signed)).ToLowerInvariant();

    // constant-time compare
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature));
}
Ruby
require 'openssl'

# raw_body: the request body exactly as received (String), never re-serialised JSON.
# timestamp / signature: the X-Webhook-Timestamp and X-Webhook-Signature header values.
def valid?(secret, timestamp, raw_body, signature)
  return false unless timestamp.match?(/\A\d+\z/)
  return false if (Time.now.to_i - timestamp.to_i).abs > 300 # replay guard

  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")
  return false unless expected.bytesize == signature.bytesize

  OpenSSL.fixed_length_secure_compare(expected, signature) # constant-time compare
end

तैयार helper चाहिए? आधिकारिक SDKs signature जाँचकर इवेंट parse भी कर देते हैं: Node.js, Python, PHP, Java, Go, .NET और Ruby।

Reference

इवेंट के प्रकार

हर डिलीवरी में इनमें से एक नाम होता है, जो payload के coreEventType से निकाला गया है।

इवेंटअर्थ
order.createdएक ऑर्डर बनाया गया।
order.payment_confirmedकिसी ऑर्डर का पेमेंट कन्फ़र्म हुआ।
order.fulfilment_startedकिसी ऑर्डर की fulfilment शुरू हो गई है।
order.fulfilment_completedएक ऑर्डर सफलतापूर्वक पूरा हुआ।
order.failedएक ऑर्डर विफल हुआ।
order.cancelledएक ऑर्डर रद्द किया गया।
order.refund_completedकिसी ऑर्डर का refund पूरा हुआ।
order.document_readyबना हुआ दस्तावेज़ (जैसे e-Stamp सर्टिफ़िकेट) डाउनलोड के लिए तैयार है।
Payload

उदाहरण

order.fulfilment_completed

JSON
{
  "coreOrderId": "core_order_9f3a1c2b8e4d4a5f",
  "coreEventType": "FULFILMENT_COMPLETED",
  "occurredAt": "2026-09-25T09:05:00.000Z",
  "orderId": "ord_4f5a6b7c8d9e0f1a",
  "externalOrderId": "ord_ext_123",
  "status": "FULFILMENT_COMPLETED"
}
जल्द आ रहा है

पार्टनरों के लिए एक API, जिससे वे अपने webhook endpoints खुद रजिस्टर, सूचीबद्ध और टेस्ट कर सकें (बनाना, secret rotate करना, हटाना)। आज किसी organization के webhook endpoints StampMitra आंतरिक रूप से कॉन्फ़िगर करता है। Backend में अभी POST /v1/webhooks जैसा कोई पुष्ट public route नहीं है।