Webhooks
Webhooks are the source of truth for payment status. The hosted checkout may close before the customer's browser hits your successUrl — the webhook always fires.
Register an endpoint
Dashboard → Developers → Webhooks → Add endpoint. Save the signing secret somewhere safe; you'll use it to verify signatures.
Event types
| Event | When |
|---|---|
payment.success | Transaction captured — invoice issued if auto-issue enabled |
payment.failed | Authorization declined or gateway error |
payment.refunded | Full refund completed — linked invoice voided |
payment.partially_refunded | Partial refund completed |
payment.cancelled | Customer abandoned checkout |
payment.requires_action | 3-D Secure required — customer needs to come back |
Payload
{
"event": "payment.success",
"transactionId": "8f8a…",
"amount": 1000.00,
"currency": "SAR",
"status": "SUCCESS",
"billId": "a1b2c3d4-…",
"billNumber": "INV-2026-00042",
"totalCost": 1000.00,
"totalVat": 130.43,
"totalWithoutVat": 869.57
}Bill fields (billId, billNumber, totalCost, totalVat, totalWithoutVat) are present when a VAT invoice was issued for the transaction. See Educational billing.
Verifying the signature
minnha signs every webhook with HMAC-SHA256 of the raw body, using your endpoint's secret. The signature is sent in the X-Minnha-Signature header.
import { Minnha, MinnhaSignatureError } from "@minnha/node";
app.post(
"/webhooks/minnha",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = Minnha.Webhooks.parse(
req.body, // RAW Buffer (not parsed)
req.header("x-minnha-signature")!,
process.env.MINNHA_WEBHOOK_SECRET!
);
switch (event.event) {
case "payment.success":
if (event.billNumber) {
// link invoice INV-2026-00042 to your order
}
markOrderPaid(event.transactionId);
break;
case "payment.failed": markOrderFailed(event.transactionId); break;
case "payment.refunded": refundOrder(event.transactionId); break;
}
res.status(200).end(); // ALWAYS respond 200 once processed
} catch (err) {
if (err instanceof MinnhaSignatureError) return res.status(400).send("bad signature");
throw err;
}
}
);Retries
If your endpoint returns anything other than 2xx within 5 seconds, minnha retries with exponential backoff for up to 24 hours. Deduplicate on transactionId + event.
Inspecting deliveries
Dashboard → Developers → Webhooks → Recent deliveries shows the last 100 deliveries with response codes. Click any failed delivery to Retry manually.