Callbacks and Local Testing
Which endpoints each flow needs
Section titled “Which endpoints each flow needs”| Flow | Endpoints you expose | Why they exist |
|---|---|---|
| STK Push | callbackUrl | final payment success or failure |
| C2B | validationUrl, confirmationUrl | pre-check and final payment receipt |
| B2C | resultUrl, queueTimeOutUrl | final result and timeout notification |
| Account balance | resultUrl, queueTimeOutUrl | final async result and timeout notification |
| Transaction status | resultUrl, queueTimeOutUrl | final async result and timeout notification |
| Reversal | resultUrl, queueTimeOutUrl | final async result and timeout notification |
- Every callback endpoint should be public, stable, and served over HTTPS.
- The SDK validates callback payload shapes, but you still own idempotency, persistence, and business decisions.
Callback handling pattern
Section titled “Callback handling pattern”flowchart TD
A[Daraja sends callback] --> B[Expose a stable public HTTPS endpoint]
B --> C[Parse and validate payload with PesaKit]
C --> D[Persist raw payload and correlation IDs]
D --> E{Outcome clear?}
E -->|Yes| F[Update payment or payout state]
E -->|No| G[Mark for reconciliation or manual review]
F --> H[Return HTTP 200 quickly]
G --> H
Use parsers inside your existing routes
Section titled “Use parsers inside your existing routes”import { C2B_VALIDATION_ACCEPT, C2B_VALIDATION_REJECT, getResultParametersMap, getStkMetadata, parseC2BConfirmation, parseC2BValidation, parseDarajaResult, parseStkPushCallback,} from "@landelatech/pesakit";
app.post("/mpesa/stk", (req, res) => { const payload = parseStkPushCallback(req.body); const metadata = getStkMetadata(payload); console.log(metadata); res.json({ ResultCode: 0, ResultDesc: "Success" });});
app.post("/mpesa/c2b/validate", (req, res) => { const payload = parseC2BValidation(req.body); const accepted = payload.BillRefNumber.startsWith("invoice-"); res.json(accepted ? C2B_VALIDATION_ACCEPT : C2B_VALIDATION_REJECT);});
app.post("/mpesa/c2b/confirm", (req, res) => { const payload = parseC2BConfirmation(req.body); console.log(payload.TransID, payload.TransAmount); res.send("OK");});
app.post("/mpesa/b2c/result", (req, res) => { const payload = parseDarajaResult(req.body); const parameters = getResultParametersMap(payload); console.log(payload.Result.ResultCode, parameters); res.send("OK");});
app.post("/mpesa/b2c/timeout", (req, res) => { console.log("Daraja timeout", req.body); res.send("OK");});Or let the SDK create the HTTP handler
Section titled “Or let the SDK create the HTTP handler”import { createServer } from "node:http";import { C2B_VALIDATION_ACCEPT, c2BConfirmationRoute, c2BValidationRoute, createCallbackHandler, darajaResultRoute, stkPushRoute,} from "@landelatech/pesakit";
const handler = createCallbackHandler({ routes: { "/mpesa/stk": stkPushRoute((payload) => { console.log(payload.CheckoutRequestID); }), "/mpesa/c2b/validate": c2BValidationRoute(() => ({ body: C2B_VALIDATION_ACCEPT, })), "/mpesa/c2b/confirm": c2BConfirmationRoute((payload) => { console.log(payload.TransID); }), "/mpesa/b2c/result": darajaResultRoute((payload) => { console.log(payload.Result.OriginatorConversationID); }), },});
createServer(handler).listen(3000);What each endpoint should return
Section titled “What each endpoint should return”- STK callback: return HTTP
200as soon as you have safely persisted the payload. - C2B validation: return JSON with
ResultCodeandResultDesc. UseC2B_VALIDATION_ACCEPT,C2B_VALIDATION_REJECT, orc2bValidationResponse()for a specific Daraja rejection code. - C2B confirmation: return
200after persisting the payment event. - B2C, balance, status, and reversal result URLs: return
200after storing the async result. - Timeout URLs: return
200, record the timeout, and schedule reconciliation. - If your server returns
503or is unavailable, Daraja may discard the callback result instead of replaying it indefinitely. - For C2B validation specifically, do not do slow downstream work in-line. Respond quickly or M-Pesa may fall back to the registered default action.
How to configure the URLs
Section titled “How to configure the URLs”- STK: set
callbackUrlinsidempesa.stkPush(). - C2B: register
validationUrlandconfirmationUrlonce withmpesa.c2b.registerUrls(). - B2C, account balance, transaction status, and reversal: pass
resultUrlandqueueTimeOutUrlin each request.
C2B URL rules worth following
Section titled “C2B URL rules worth following”- Production C2B URLs should be HTTPS.
- Sandbox can be tested over HTTP, though HTTPS is still preferable.
- Use your own stable application domains or IPs, not public URL catcher tools.
- Avoid URL patterns Safaricom flags, including names based on
mpesa,safaricom,sql,query,cmd, or similar variants.
Local testing
Section titled “Local testing”- Run the local callback server on a fixed port.
- Expose it with a tunnel such as ngrok or another HTTPS-capable tunnel.
- Register the public URL in the Daraja portal or request body, depending on the API.
- Keep sandbox and production callback domains separate so you do not mix live traffic with tests.
- For production C2B registration, move from testing tunnels to owned URLs before go-live.
Callback IP allowlisting
Section titled “Callback IP allowlisting”If your infrastructure restricts inbound traffic, allow Daraja callback traffic from these Safaricom gateway IPs:
196.201.214.200196.201.214.206196.201.213.114196.201.214.207196.201.214.208196.201.213.44196.201.212.127196.201.212.138196.201.212.129196.201.212.136196.201.212.74196.201.212.69