Payment Integration
M-Pesa Payment Webhooks: Mark Orders Paid Automatically
When a customer's M-Pesa payment ends, KenZobe Checkout can tell your server straight away with a signed message called a webhook. Your system marks the order paid and releases it, without anyone checking by hand. Here is how it works, what "paid" really means, and how to tell a real event from a fake one.
Building it now?
This post explains the ideas. The full webhooks developer guide, with every field and working verification code, is in your KenZobe dashboard. Sign in to read it.
What a webhook is, and why your shop needs one
A webhook is a message one system sends to another when something happens. With KenZobe Checkout, the "something" is a payment ending: paid, failed, cancelled or timed out. KenZobe sends one POST request with a JSON body to an https address on your server that you choose, for example https://bookstore.example.com/kenzobe/webhook.
Picture an online bookstore. A customer orders a book, taps the Pay with M-Pesa button, gets the STK prompt and enters their PIN. Without a webhook, someone at the shop has to check the dashboard or the Paybill before packing the order. With one, the bookstore's server hears payment.paid, finds the order and marks it paid on its own. The money itself goes straight into the shop's own Paybill or Till; KenZobe never holds it. The webhook is just the news.
Your server can also ask instead of being told: look the payment up with the KenZobe API and your secret key, as the payment check in the pay button guide shows. You can use both: the webhook to act the moment a payment ends, and a lookup when you want to check one payment again.
Paid means confirmed by Safaricom
Only one event means money reached your Paybill or Till: payment.paid. KenZobe sends it only after Safaricom has confirmed the payment, never on a callback alone. So when your server receives a genuine payment.paid, you can release the order.
There are four events, one for each way a payment can end:
payment.paid: Safaricom confirmed the payment. Fulfil the order.payment.failed: Safaricom declined it, or the prompt could not be sent. Let the customer try again.payment.cancelled: the customer cancelled the prompt on their phone. Let them try again.payment.timed_out: no answer came in time. Let them try again.
Each payment gets one final event, and its status never changes after that. Failed, cancelled and timed-out events also carry a reason (wrong PIN, not enough money in the M-Pesa account, phone off and so on) and a sentence you can show the customer. The full list of failure reasons, and what to do about each, is in the developer guide.
What an event looks like
The body is the payment itself, in the same shape the API returns when you look a payment up. A shortened payment.paid event:
{
"id": "evt_7Qe2sV9kLm4Xb1Rt8NcPz3Hd",
"type": "payment.paid",
"mode": "live",
"data": {
"id": "pay_Xk3mP9qR2vT8wL5nB7cY4zH1",
"status": "paid",
"amount": 1500,
"currency": "KES",
"client_reference": "order-1042",
"mpesa_receipt": "TJT4ABC12X",
…
}
}Three things to notice:
client_referenceis your own order number, set on the pay button withdata-kenzobe-client-reference. It is how the bookstore finds order-1042.amountis whole Kenya shillings. Check it matches the order before you release it.modeistestorlive. Never fulfil an order from atestevent.
The customer's phone number is masked in every event; the full number is never sent. The developer guide lists every field and what it means.
How you know an event is really from KenZobe
Your webhook address is on the public internet, so anyone can send it a request that looks like payment.paid. If your server believed every request, a stranger could get an order released for free. That is why every KenZobe event is signed.
When you add an endpoint, KenZobe gives you a signing secret (it starts with whsec_). Only you and KenZobe know it. Each event arrives with a KenZobe-Signature header holding a time and a signature. The idea in four lines:
KenZobe-Signature: t=<unix time>,v1=<hex signature>
signed text: <t>.<raw body>
expected: HMAC-SHA256 of the signed text with your signing secret
accept only: expected matches v1, and t is within 5 minutes of your clock- The signature proves who sent it. Without your secret, nobody can make a signature that matches.
- It also proves nothing was changed. Change one character of the body and the signature no longer matches. That is why you check the raw body exactly as received, before any JSON parsing.
- The time stops replays. Rejecting events more than 5 minutes old means a captured event cannot be sent again later.
Reject anything that fails the check with a 400 and do nothing with it. Ready-to-use verification code for Node.js (Express), PHP (including WordPress) and Python (Flask) is in the developer guide.
Answer fast, and expect retries and repeats
- Answer quickly. Reply with any
2xxstatus within 10 seconds. Send emails and update stock after you answer, or in a background job. - Missed deliveries are retried. If your server is down or answers with an error, KenZobe tries again: seven attempts over about 21 hours. If they all fail, everyone confirmed on your KenZobe account gets one email, and you can resend the event from the dashboard.
- The same event can arrive more than once. Save each event id (
evt_…) you handle. When the same id comes again, answer200and do nothing, so one payment releases one order. - Redirects are not followed. Give the final address, with the right
wwwand trailing slash.
Test and live are separate
You add endpoints in the KenZobe dashboard under API keys → Webhooks. The address must be https and reachable from the internet; addresses on localhost or inside a private network are refused. The signing secret is shown only once, so copy it straight away and keep it on your server, for example in an environment variable.
A test endpoint hears about test payments only, and a live endpoint about real payments only, each with its own secret. So you can build and test the whole flow free in sandbox, then add a live endpoint once live payments are open on your account. Each payment's page in the dashboard lists its webhook deliveries with your server's answer or the error.
Read the full webhooks developer guide
Everything you need to build a webhook endpoint you can trust, in your KenZobe dashboard:
- Every field in an event, and what each one means
- Every failure reason, and what to tell the customer
- Signature verification code for Node.js (Express), PHP (including WordPress) and Python (Flask)
- The full retry schedule, and how to recover missed events
- A go-live checklist, from test endpoint to your first real payment
Sign-in required. If you are signed out, you go to sign in first and then straight back to the guide.
Calling Daraja yourself?
If you send STK pushes from your own server with Safaricom's Daraja API, there is no KenZobe webhook: Safaricom sends its result to your callback URL, and confirming it is your job. Our guide to M-Pesa callback URLs covers that route. For every way to take M-Pesa online, see how to accept M-Pesa payments on your website.
Frequently Asked Questions
What is an M-Pesa payment webhook?+
A message KenZobe sends to your own server when a customer’s M-Pesa payment ends: paid, failed, cancelled or timed out. It is a POST request with a JSON body to an https address you choose, so your system can mark the order paid without anyone checking by hand.
When does KenZobe send payment.paid?+
Only after Safaricom has confirmed the payment, never on a callback alone. payment.paid is the only event that means the money reached your Paybill or Till.
How do I know a webhook really came from KenZobe?+
Every event is signed. The KenZobe-Signature header carries a time and an HMAC-SHA256 signature made with your endpoint’s signing secret. Your server works out the same signature from the raw body and rejects the event if they differ or if the time is more than 5 minutes from your server’s clock.
What happens if my server is down when a payment ends?+
KenZobe retries the delivery, seven attempts over about 21 hours. If every attempt fails, the delivery is marked failed and everyone confirmed on your KenZobe account gets one email. You can then resend the event from the dashboard or read the payment from the API with your secret key.
Can the same webhook arrive twice?+
Yes. Save the id of each event you handle (it starts with evt_) and answer 200 without doing anything when the same id comes again. That way one payment releases one order.
Does it work with PHP and WordPress?+
Yes. Any server that can take an https POST request can receive KenZobe webhooks. The full developer guide has signature-checking code for Node.js (Express), for PHP (including WordPress) and for Python (Flask).
Where is the full webhooks developer guide?+
Inside the KenZobe dashboard at app.kenzobe.com/docs/webhooks. You need to be signed in to read it; if you are not, you are sent to sign in and then straight back to the guide. Sign-up is free and starts in sandbox.
Get started
Sign in and open the developer guide, or create a free account and test webhooks in sandbox. Test and live endpoints are separate, each with its own signing secret.