Search pages in the SMS Pay documentation.
Webhooks notify your backend when a payment status changes. Use webhooks for fulfillment instead of relying on browser polling. Webhook delivery is at-least-once, so your handler must be idempotent.
Common events:
| Event | Meaning |
|---|---|
payment.paid | A payment intent was confirmed. |
payment.review_required | Matching was ambiguous and needs manual review. |
payment.expired | The payment intent expired. |
payment.rejected | A reviewed payment was rejected. |
webhook.endpoint_verification | Verification challenge for a new endpoint. |
2xx only after the event is safely recorded.POST /v1/webhooks/endpoints
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"url": "https://merchant-site.com/webhooks/sms-pay",
"secret": "whsec_minimum_16_characters"
}
{
"id": "wh_123",
"url": "https://merchant-site.com/webhooks/sms-pay",
"environment": "SANDBOX",
"isVerified": false,
"isActive": true,
"createdAt": "2026-05-05T10:00:00.000Z"
}
New endpoints start unverified. Only active, verified endpoints receive payment events.
POST /v1/webhooks/endpoints/wh_123/verify
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
The gateway sends:
{
"event": "webhook.endpoint_verification",
"environment": "SANDBOX",
"timestamp": "2026-05-05T10:00:00.000Z",
"challenge": "9c2f8d..."
}
Your endpoint should return the challenge:
{
"challenge": "9c2f8d..."
}
| Header | Description |
|---|---|
X-Webhook-Id | Unique delivery ID. Store it to avoid duplicate processing. |
X-Webhook-Event | Event type, for example payment.paid. |
X-Webhook-Signature | HMAC signature as sha256=<hex>. |
X-Webhook-Timestamp | Delivery timestamp. |
User-Agent | smspaybd-webhooks/1.0. |
{
"event": "payment.paid",
"environment": "SANDBOX",
"timestamp": "2026-05-05T10:01:00.000Z",
"data": {
"payment_intent_id": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"amount": "500",
"currency": "BDT",
"customer_reference": "ORDER-10045"
}
}
Node and Express signature verification:
import crypto from "node:crypto";
import express from "express";
const app = express();
const webhookSecret = process.env.SMS_PAY_WEBHOOK_SECRET!;
function safeEqual(left: string, right: string) {
const a = Buffer.from(left);
const b = Buffer.from(right);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post(
"/webhooks/sms-pay",
express.raw({ type: "application/json" }),
async (req, res) => {
const rawBody = req.body as Buffer;
const signature = req.header("x-webhook-signature") ?? "";
const expected =
"sha256=" +
crypto.createHmac("sha256", webhookSecret).update(rawBody).digest("hex");
if (!safeEqual(signature, expected)) {
return res.status(401).send("invalid signature");
}
const webhookId = req.header("x-webhook-id");
const eventType = req.header("x-webhook-event");
const payload = JSON.parse(rawBody.toString("utf8"));
if (eventType === "webhook.endpoint_verification") {
return res.json({ challenge: payload.challenge });
}
// Store webhookId and process idempotently.
if (eventType === "payment.paid") {
// Mark your order paid after verifying the payment intent if needed.
}
return res.status(200).send("ok");
},
);
GET /v1/webhooks/deliveries
X-Api-Key: sk_test_xxxxxxxxxxxxxxxxx
{
"data": [
{
"id": "delivery_123",
"endpointId": "wh_123",
"paymentIntentId": "b5012f33-207e-4999-bf8d-5a1ebb10988e",
"eventType": "payment.paid",
"status": "DELIVERED",
"attempt": 1,
"statusCode": 200,
"responseBody": "ok",
"idempotencyKey": "payment.paid:b5012f33-207e-4999-bf8d-5a1ebb10988e",
"deliveredAt": "2026-05-05T10:01:00.000Z",
"createdAt": "2026-05-05T10:01:00.000Z"
}
],
"total": 1
}
| Status | Meaning |
|---|---|
PENDING | Delivery is queued. |
DELIVERED | Endpoint returned a successful 2xx response. |
RETRYING | Previous attempt failed and another attempt is scheduled. |
FAILED | Delivery failed permanently or exhausted retries. |
X-Webhook-Signature.2xx only when you want the gateway to retry.