SMS PaySearch pages in the SMS Pay documentation.
Set up a webhook endpoint so SMS Pay can notify your backend when payment status changes. Merchant admins configure endpoints in the dashboard; developers implement the receiving route.
https://merchant.example.com/webhooks/sms-pay.Production endpoints must use HTTPS.
Subscribe to:
| Event | Recommended handling |
|---|---|
payment.paid | Mark the order paid and fulfill. |
payment.review_required | Alert operations; do not fulfill. |
payment.expired | Mark the payment attempt expired. |
payment.rejected | Mark the attempt rejected after review. |
payment.refunded | Reverse fulfillment or record the refund; the payment was refunded after it was paid. |
payment.cancelled | Release held stock; the customer cancelled on the hosted checkout. |
For most merchants, one endpoint subscribed to all six events is simplest.
SMS_PAY_WEBHOOK_SECRET=whsec_minimum_16_characters
Keep the secret private. Rotate it if it is exposed.
Verify X-Webhook-Signature against the raw request body before parsing JSON.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = 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", secret).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"));
await saveWebhookOnce(webhookId, payload);
if (eventType === "payment.paid") {
await markOrderPaid({
smsPayPaymentIntentId: payload.data.payment_intent_id,
merchantReference: payload.data.merchant_reference,
});
}
return res.status(200).send("ok");
},
);
{
"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": "SP7A91BC2D0",
"merchant_reference": "ORDER-10045"
}
}
Use merchant_reference to find your order when available. Use payment_intent_id as the stable SMS Pay fallback.
Verify checks whether your endpoint can receive SMS Pay verification requests. Verification is a setup health check; it does not prove your fulfillment logic is complete.
Test sends a selected event to your endpoint and records the delivery result. Use it to confirm:
2xxDashboard -> Webhooks shows delivery status, HTTP status, attempts, payload, response body, and retry state. Use this page when an order did not update in your system.
Any non-2xx response, timeout, or redirect counts as a failed attempt. SMS Pay retries failed deliveries 10 times over about 24 hours (30s, 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 10h) and then marks the delivery FAILED. You can retry a failed delivery from the dashboard. Webhook URLs must be public HTTPS endpoints. Redirects are not followed, and private or internal addresses are rejected.