API reference
M-Pesa payment webhooks: events and signatures
The event envelope, the delivery headers, the HMAC signature scheme, and every event type paylod can send you.
Webhooks are optional. Register a webhook endpoint on the application's Endpoints & Webhooks tab. paylod then sends a signed event as soon as a payment settles. If you do not want to run a webhook endpoint, poll `GET /status/:id` instead. paylod records every payment in both cases.
Delivery headers
| Header | Description |
|---|---|
x-webhook-event | The event type, e.g. payment.success. |
x-webhook-id | Unique delivery id. Use it to dedupe retries. |
x-webhook-signature | HMAC signature, format t=<unix>,v1=<hex>. |
Event types
| Event | Fired when |
|---|---|
payment.success | A collection settled. The source is an STK Push or an offline C2B payment. |
payment.failed | A collection failed. The customer cancelled it, or it timed out, or the PIN was wrong, or the funds were not sufficient. |
payout.result | A `/payout` reached a final state. |
refund.result | A `/reversal` reached a final state. |
admin.transaction_status | A `/transaction-status` query returned an answer. |
admin.account_balance | An `/account-balance` query returned an answer. |
Every event uses the same envelope and the same signature scheme.
The envelope
POST https://your-app.com/webhooks/mpesa
x-webhook-event: payment.success
x-webhook-id: 8f3c…
x-webhook-signature: t=1751394302,v1=5a2f…c9
{
"type": "payment.success",
"created": 1751394302,
"data": {
"paymentId": "e69e5c00-…",
"applicationId": "…",
"env": "production",
"status": "success",
"amount": 10,
"phone": "254712345678",
"accountRef": "INV-2041",
"mpesaReceipt": "UG1F3A1U7J",
"checkoutRequestId": "ws_CO_…",
"resultCode": 0,
"resultDesc": "The service request is processed successfully.",
"decoded": null
}
}The data object carries exactly these fields. It has no metadata field. The webhook does not return the opaque metadata that you pass to collect(). Key your fulfilment on paymentId or on accountRef. On a payment.failed event, decoded is a { code, title, cause, fix, category, retryable, customerMessage } object. paylod decodes that object from the Safaricom result code. On a payment.success event, decoded is null.
How to verify the signature
The signature is an HMAC-SHA256 over the string t + "." + rawBody. The key is the endpoint's signing secret. paylod shows that secret once, when you add the endpoint. Every official SDK verifies the signature for you and returns a typed event. Compute the HMAC yourself only if you use another runtime.
const event = paylod.verifyWebhook({
payload: rawBody, // the RAW request bytes
signature: headers["x-webhook-signature"], // "t=...,v1=..."
secret: process.env.PAYLOD_WEBHOOK_SECRET,
});
// event.type, event.data.paymentId, event.data.mpesaReceiptThe verification has three rules. Apply the three rules in this order:
- Compute the HMAC over the raw request body. Do not use a re-serialised object.
- Compare the result in constant time against
v1. - Reject a delivery if its
tis more than about five minutes old. This rule limits replay attacks.
How to respond
Respond 2xx to acknowledge the event. If you do not respond 2xx, paylod retries the delivery with backoff. The dashboard shows every attempt. paylod delivers each event at least once. Dedupe on x-webhook-id.
Verify the signature. Then compare data.amount against your own order total. Do both steps before you fulfil the order. The webhook always carries the true settled amount. See Secure integration.