paylod
Webhooks

API reference

M-Pesa payment webhooks: events and signatures

.md

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

HeaderDescription
x-webhook-eventThe event type, e.g. payment.success.
x-webhook-idUnique delivery id. Use it to dedupe retries.
x-webhook-signatureHMAC signature, format t=<unix>,v1=<hex>.

Event types

EventFired when
payment.successA collection settled. The source is an STK Push or an offline C2B payment.
payment.failedA collection failed. The customer cancelled it, or it timed out, or the PIN was wrong, or the funds were not sufficient.
payout.resultA `/payout` reached a final state.
refund.resultA `/reversal` reached a final state.
admin.transaction_statusA `/transaction-status` query returned an answer.
admin.account_balanceAn `/account-balance` query returned an answer.

Every event uses the same envelope and the same signature scheme.

The envelope

Example delivery
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.mpesaReceipt

The verification has three rules. Apply the three rules in this order:

  1. Compute the HMAC over the raw request body. Do not use a re-serialised object.
  2. Compare the result in constant time against v1.
  3. Reject a delivery if its t is 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.