Complete reference to ZezoPay's real-time webhook engine, HMAC SHA-256 signature verification, retry policies, and event schemas.
When customers initiate or complete transactions on ZezoPay, our webhook engine dispatches asynchronous HTTP POST notifications in real-time to your configured destination endpoint.
Webhooks provide deterministic, server-to-server confirmation for order fulfillment, subscription provisioning, and entitlement activations without relying on frontend browser redirects.
ZezoPay sends requests with a Content-Type: application/json payload and custom security headers to identify requests and verify authenticity.
| Header Name | Type | Description |
|---|---|---|
Content-Type | string | Always application/json; charset=utf-8. |
x-zezopay-request-id | string | Unique identifier for each webhook delivery attempt (for deduplication). |
x-zezopay-webhook-signature | string | Hex-encoded HMAC SHA-256 signature generated using your Webhook Secret. |
HMAC Signature Verification
Always verify the x-zezopay-webhook-signature on incoming webhooks to guarantee the payload originated from ZezoPay and was not tampered with in transit.
All ZezoPay webhook payloads share a standard top-level JSON envelope:
{
"data": {
"entity": "event",
"account_id": "acc_66d2e734c67cf10fd3b46b86",
"event": "payment.paid",
"contains": ["payment"],
"created_at": 1725264000,
"payload": {
"payment": {
"entity": {
"id": "pay_66d3c812a01b",
"entity": "payment",
"created_at": 1725264000000,
"price": 1000,
"currency": "INR",
"status": "paid",
"order_id": "order_NX8aBz10Q1pLMq",
"pw_id": "rzp_pay_9912059abc",
"payment_gateway": "razorpay",
"user_id": "usr_9912059abc",
"user_name": "Sarah Connor",
"user_email": "sarah.connor@example.com",
"user_phone": "+919876543210",
"meta_data": {}
}
}
}
}
}Timestamp Formats
data.created_at: Unix timestamp in seconds (e.g. 1725264000).entity.created_at / start_date / end_date: Unix timestamps in milliseconds (e.g. 1725264000000).data.payload corresponds to the entity type: payment, subscription, or digital_product.ZezoPay provides 25+ granular webhook events categorized across checkout payments, recurring subscriptions, and digital product purchases.
| Event Name | Description | Triggered When |
|---|---|---|
payment.created | Payment record initialized | Customer opens the checkout modal or begins payment session. |
payment.attempted | Payment attempt submitted | Customer submits card, UPI, or banking credentials to gateway. |
payment.authorized | Payment authorized by bank | Funds authorized by issuing bank, pending capture. |
payment.paid | Payment successfully captured | Transaction is fully paid and settled. Fulfill order here. |
payment.pending | Payment pending settlement | Asynchronous rail (e.g. Bank Transfer/Offline) awaiting confirmation. |
payment.failed | Payment failed or rejected | Insufficient funds, gateway error, or bank decline. |
payment.cancelled | Payment session abandoned | Customer cancelled checkout or window timed out. |
payment.refunded | Transaction refunded | Partial or full refund issued back to customer. |
payment.chargeback | Chargeback initiated | Customer filed a dispute or chargeback with issuing bank. |
| Event Name | Description | Triggered When |
|---|---|---|
subscription.active | Subscription activated / renewed | Customer completes initial charge or recurring billing cycle succeeds. |
subscription.trial | Free trial period started | Customer enrolls in a plan with trial duration. |
subscription.pending | Renewal payment pending | Downstream gateway is processing recurring payment. |
subscription.failed | Renewal charge failed | Dunning cycle initiated due to expired card or payment failure. |
subscription.canceled | Subscription cancelled | Customer or admin terminates recurring subscription. |
subscription.inactive | Subscription paused | Subscription paused temporarily without full cancellation. |
subscription.expired | Subscription duration ended | Billing cycle finished without active auto-renewal. |
| Event Name | Description | Triggered When |
|---|---|---|
product.purchase.paid | Digital product purchased | Payment confirmed. Grant digital download / course access. |
product.purchase.trial | Product trial access granted | Customer starts complimentary trial access period. |
product.purchase.pending | Purchase awaiting payment | Async offline payment pending confirmation. |
product.purchase.failed | Product purchase failed | Transaction declined by gateway during checkout. |
product.purchase.cancelled | Purchase cancelled / revoked | Order cancelled by merchant or customer. |
product.purchase.active | Access actively valid | Digital access is active and valid. |
product.purchase.inactive | Access paused or locked | Access temporarily paused or frozen. |
product.purchase.expired | Digital entitlement expired | License duration or access period (e.g. 365 days) elapsed. |
payload.payment.entity){
"id": "pay_66d3c812a01b",
"entity": "payment",
"created_at": 1725264000000,
"price": 1000,
"currency": "INR",
"status": "paid",
"order_id": "order_NX8aBz10Q1pLMq",
"pw_id": "rzp_pay_9912059abc",
"payment_gateway": "razorpay",
"user_id": "usr_9912059abc",
"user_name": "Sarah Connor",
"user_email": "sarah.connor@example.com",
"user_phone": "+919876543210",
"meta_data": {
"isPaymentInitiatedEnabled": true
}
}payload.subscription.entity){
"id": "sub_66d3b980f72a",
"entity": "subscription",
"created_at": 1725264000000,
"plan_id": "plan_66d3a12b4e89",
"plan_name": "Pro Monthly",
"plan_duration": 30,
"plan_duration_unit": "days",
"plan_price": 2999,
"price": 2999,
"payment_id": "pay_66d3c812a01b",
"status": "active",
"order_id": "order_NX8aBz10Q1pLMq",
"payment_gateway": "stripe",
"user_id": "usr_9912059abc",
"user_name": "Sarah Connor",
"user_email": "sarah.connor@example.com",
"user_phone": "+919876543210",
"pw_id": "sub_1PvAbc2eZvKYlo2C",
"start_date": 1725264000000,
"end_date": 1727856000000,
"meta_data": {}
}payload.digital_product.entity){
"id": "prod_purch_66d3c812a01b",
"entity": "digital_product",
"created_at": 1725264000000,
"product_id": "prod_66d3a9921e10",
"product_name": "Fullstack Next.js Course",
"product_price": 499,
"price": 499,
"currency": "INR",
"product_expiry": "365",
"product_expiry_unit": "days",
"product_slug": "nextjs-mastery",
"product_description": "Comprehensive course access with source code repository",
"product_category": "Education",
"product_purchase_id": "prod_purch_66d3c812a01b",
"status": "active",
"payment_id": "pay_66d3c812a01b",
"order_id": "order_NX8aBz10Q1pLMq",
"payment_gateway": "razorpay",
"user_id": "usr_9912059abc",
"user_name": "Sarah Connor",
"user_email": "sarah.connor@example.com",
"user_phone": "+919876543210",
"pw_id": "rzp_pay_9912059abc",
"meta_data": {}
}Verify incoming webhook authenticity using HMAC SHA-256 with your Webhook Secret configured in your ZezoPay dashboard.
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
function verifyWebhookSignature(payload, signature, secret) {
if (!secret) return true;
const computedSignature = crypto
.createHmac("sha256", secret)
.update(JSON.stringify({ data: payload }))
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computedSignature, "utf8"),
Buffer.from(signature, "utf8")
);
}
app.post("/api/webhooks/zezopay", (req, res) => {
const signature = req.headers["x-zezopay-webhook-signature"];
const webhookSecret = process.env.ZEZOPAY_WEBHOOK_SECRET;
if (!signature || !verifyWebhookSignature(req.body.data, signature, webhookSecret)) {
return res.status(401).json({ error: "Invalid signature" });
}
const { event, payload } = req.body.data;
switch (event) {
case "payment.paid":
console.log("Payment captured:", payload.payment.entity);
// Fulfill order, deliver items
break;
case "subscription.active":
console.log("Subscription activated:", payload.subscription.entity);
// Provision premium access
break;
case "product.purchase.paid":
console.log("Product purchased:", payload.digital_product.entity);
// Unlock digital product access
break;
case "payment.failed":
console.log("Payment failed:", payload.payment.entity);
break;
default:
console.log("Unhandled event type:", event);
}
// Acknowledge receipt within 10 seconds
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log("Webhook listener running on port 3000"));If your webhook endpoint returns an HTTP status outside the 2xx range (or takes longer than 10 seconds to respond), ZezoPay automatically queues retries with exponential backoff:
200 OK Immediately: Parse the event and queue downstream heavy tasks (e.g. sending emails or PDF generation) asynchronously using background workers.x-zezopay-request-id or entity.id to prevent duplicate operations in case of network retries.https://) for webhook reception in production.