Payment Gateway
Webhooks Reference
Four Critical Rules Before You Begin
- 1. Verify against raw request bytes. Do not parse JSON before verifying HMAC. In Express, mount
express.raw({ type: "application/json" })on your webhook route before any globalexpress.json()parser. - 2. Return HTTP 2xx within 10 seconds. Return a 200 immediately, then process asynchronously in background workers to prevent delivery timeouts and retries.
- 3. Deduplicate using
event.id. Deliveries are at-least-once. Theevt_...identifier is strictly stable across retries and replayed events. - 4. Timestamps are in milliseconds. The
createdfield is a 13-digit Unix millisecond epoch timestamp, not seconds.
🔒Signature format specification: The signature string is formed as
t={timestamp},{body} (comma-delimited). Compute the HMAC-SHA256 hex digest using the exact raw payload bytes.Verifying Signatures
import crypto from "node:crypto";
export function verifyWebhookSignature({
rawBody,
signatureHeader,
secret,
toleranceSeconds = 300,
}: {
rawBody: string | Buffer;
signatureHeader: string;
secret: string;
toleranceSeconds?: number;
}): boolean {
// Parse header: t=1759001742000,v1=9f8e...
const parts = signatureHeader.split(",");
let timestamp = 0;
let signature = "";
for (const part of parts) {
const [k, v] = part.split("=");
if (k === "t") timestamp = parseInt(v, 10);
if (k === "v1") signature = v;
}
if (!timestamp || !signature) return false;
// Replay tolerance check
const now = Date.now();
if (Math.abs(now - timestamp) > toleranceSeconds * 1000) {
return false;
}
// The signed payload is 't={timestamp},{body}'
const signedPayload = `t=${timestamp},${rawBody.toString("utf8")}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
);
}Retry Ladder & Auto-Pause
Deliveries follow a 5-attempt retry ladder with exponential backoff on HTTP errors or network timeouts:
| Attempt | Delay / Schedule | Status on Error |
|---|---|---|
| Attempt 1 | Immediate upon event emission | failed (retry scheduled) |
| Attempt 2 | 1 minute later | failed (retry scheduled) |
| Attempt 3 | 5 minutes later | failed (retry scheduled) |
| Attempt 4 | 30 minutes later | failed (retry scheduled) |
| Attempt 5 | 2 hours later (terminal) | dead (no further retries) |
Event Catalog Reference
All Events (15)
payment
payment.succeeded
When it fires:Immediately after the acquirer authorises and the charge is captured.
Example Payload (payment.succeeded)
{
"id": "evt_7d8e1a2b3c4d5e6f",
"object": "event",
"type": "payment.succeeded",
"api_version": "2026-09-27",
"created": 1759001742000,
"data": {
"object": {
"object": "payment",
"id": "pay_5f2a9c1e4b3d4a2e",
"amount": 125000,
"currency": "INR",
"status": "SUCCEEDED",
"order_ref": "order-88213",
"statement_descriptor": "MAYA PHARMA",
"payer_name": "Asha Rao",
"card": {
"brand": "visa",
"last4": "4242"
}
}
}
}