Payment Gateway

Webhooks Reference

Receive real-time signed HTTP POST notifications for payment transactions, disputes, refunds, and settlement payouts.

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 global express.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. The evt_... identifier is strictly stable across retries and replayed events.
  • 4. Timestamps are in milliseconds. The created field 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

Every webhook delivery includes an X-Paygate-Signature header containing t=... (millisecond timestamp) and v1=... (HMAC-SHA256 hex digest).

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:

AttemptDelay / ScheduleStatus on Error
Attempt 1Immediate upon event emissionfailed (retry scheduled)
Attempt 21 minute laterfailed (retry scheduled)
Attempt 35 minutes laterfailed (retry scheduled)
Attempt 430 minutes laterfailed (retry scheduled)
Attempt 52 hours later (terminal)dead (no further retries)

If an endpoint accumulates 100 consecutive terminal dead deliveries, paygate automatically transitions it to Paused status to conserve resources. You can resume and trigger Replay at any time in Payment Gateway Settings.

Event Catalog Reference

Select an event from the catalog to view its triggering scenario, target resource, and verified JSON payload envelope.

All Events (15)

payment

payment.succeeded

A payment was captured, from any channel: direct API, payment link or hosted checkout.

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"
      }
    }
  }
}