Skip to content

Daraja callback not received? Why it happens and how to fix it

The customer paid but your M-Pesa callback never arrived. The usual causes - unreachable URLs, blocked keywords, bad certificates, slow handlers - and how to recover payments safely without charging twice.

· 8 min read · Noria Technologies

The customer says they paid. They have the M-Pesa message to prove it. Your system still shows the order as unpaid, because the Daraja callback that was supposed to tell you never arrived.

This is the most common M-Pesa integration problem in Kenya, and it has a small number of causes. This guide walks through each one, how to confirm it, and how to design your system so a missing callback costs you nothing.

First, confirm the payment actually happened

Before you debug anything, find out whether the payment went through. For an STK push, call the STK push query API with the CheckoutRequestID you got when you sent the prompt:

curl https://api.safaricom.co.ke/mpesa/stkpushquery/v1/query \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "BusinessShortCode": "174379",
    "Password": "<base64 of shortcode + passkey + timestamp>",
    "Timestamp": "20261114094107",
    "CheckoutRequestID": "ws_CO_191220191020363925"
  }'

A ResultCode of 0 means the customer paid and you missed the callback. Any other code is the real outcome. If the query itself says the transaction is still being processed, wait and ask again. Either way, you now know whether you are looking for a lost payment or a failed one.

The usual causes

1. The callback URL is not publicly reachable

Safaricom calls your CallBackURL from the internet. localhost, private IP addresses, VPN-only hosts and servers behind a firewall that only allows your office will never receive it.

Check: call the URL from a machine outside your network. If you cannot reach it, neither can Safaricom.

Fix: use a public HTTPS URL. For local development, use a tunnel and expect its URL to change; production should never depend on one.

2. The URL contains a word Safaricom filters

Safaricom is widely reported to reject or silently drop callback URLs that contain certain words, such as mpesa, safaricom, sql, exec or cmd, apparently as a crude injection filter. A URL like https://shop.example/api/mpesa/callback can fail for no reason your server ever sees.

Fix: keep those words out of callback paths and query strings entirely. https://shop.example/hooks/pay/8fK2a is safer than anything descriptive.

3. The TLS certificate is invalid

Self-signed certificates, expired certificates and incomplete certificate chains make Safaricom’s request fail before it reaches your code.

Check: run your domain through an SSL checker and confirm the full chain is served.

Fix: use a certificate from a public authority, such as Let’s Encrypt, and renew it automatically.

4. Your handler is slow or returns an error

If your endpoint takes too long, throws, or returns a non-200 response, the callback may be treated as failed. Daraja does not promise to retry STK push callbacks the way most webhook providers do.

Fix: make the handler do almost nothing: verify it, write the raw body to your database, return 200, and process it afterwards. Never call another API or send an email before responding.

5. The callback arrived, but your code could not match it

This one is easy to miss. The callback did arrive, but your code looked up the order by a value that was not saved yet. A common version: you save the CheckoutRequestID only after the processrequest response returns, the response is slow, and the callback arrives first.

Fix: save your own record before you call Daraja, and put your own identifier in the callback URL so the callback can always find it, even if the CheckoutRequestID was never stored.

6. Sandbox is not production

Safaricom’s sandbox is known for slow, missing or repeated callbacks. Do not lose a week debugging behaviour that only exists in sandbox; test the recovery path instead, because production will exercise it too.

Design so a missing callback does not matter

Callbacks will go missing in production no matter how well you configure them. The fix is not a better callback; it is a system that does not depend on one:

  1. Keep the charge open. No callback means the outcome is unknown, not failed. Do not mark the order unpaid and do not cancel it.
  2. Query after a grace period. If no callback has arrived a minute or so after the prompt, ask Safaricom with the STK push query. Back off and try a few times.
  3. Never send a second prompt to cover an unknown one. If the first prompt was paid, a second one charges the customer twice.
  4. Make settlement idempotent. A late callback, your poll and a statement import may all report the same payment. Store the M-Pesa receipt number under a unique constraint so only the first one counts.
  5. Reconcile against the statement. Anything still unknown at the end of the day should be matched against your paybill or till statement, not guessed.

The guide on idempotency keys covers points 3 and 4 in more depth.

How Noria Pay handles it

Noria Pay is built around the assumption that callbacks go missing:

  • Every transaction gets its own callback URL with a secret token, stored before the request is sent, so a callback can be matched even if the dispatch response was lost.
  • Callback URLs are random codes, with no words Safaricom filters.
  • If no callback arrives within the grace period, Pay queries the transaction with Safaricom and applies the answer, backing off between attempts.
  • A request that timed out is looked up, never sent again.
  • Settlement happens once from a single open state, and the receipt index is unique, so a late callback and a poll cannot both count.
  • Anything still open can be matched from an imported statement, or looked up by its transaction code from the reconciliation screen.

Your systems get one signed webhook when the outcome is known, whichever route found it.

Quick diagnosis

Symptom Likely cause
No request ever reaches your server URL unreachable, filtered keyword or TLS problem
Requests arrive but orders stay unpaid Handler error, slow response or matching on an unsaved field
Works in production, flaky in sandbox Sandbox behaviour; test recovery instead
Customer charged twice A second prompt was sent while the first was unknown

Ready to take payments properly?

Tell us what you are building and which providers you use. We set up your workspace and help you go live.