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 द्वारा नहीं बुलाया जाता।
Exponential backoff, लगभग 13 घंटों में अधिकतम 6 प्रयास, फिर डिलीवरी dead-letter कर दी जाती है।
ऑर्डर इवेंट से आपके endpoint तक
- चरण 1
ऑर्डर इवेंट
ऑर्डर पर कुछ बदलता है
- चरण 2
Signed request
HMAC-SHA256 + timestamp header
- चरण 3
आपका endpoint
Verify करें, फिर 2xx से जवाब दें
- चरण 4
Retries
Exponential backoff, अधिकतम 6 प्रयास
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 में करें।
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);
}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
// $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
}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));
}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
}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));
}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।
इवेंट के प्रकार
हर डिलीवरी में इनमें से एक नाम होता है, जो 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 सर्टिफ़िकेट) डाउनलोड के लिए तैयार है। |
उदाहरण
order.fulfilment_completed
{
"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 नहीं है।