When you integrate payments like MPESA Daraja STK Push, the customer confirms the payment on their phone. The result (success or failure) is not immediate - Safaricom will notify your server via a callback URL.
But what about the user who is waiting on your web app? 🤔
This is where polling comes in.
🔑 Recap: MPESA STK Push Flow
- User clicks Pay Now.
- Your backend calls the Daraja STK Push API → Safaricom sends an STK prompt to the user’s phone.
- Backend saves the transaction in DB with
status = PENDING
. - Safaricom calls your
/callback
once the user accepts/declines. Backend updates status →SUCCESS
orFAILED
. - Frontend polls the backend every few seconds until the status changes from
PENDING
.
🖥️ Example Backend (Django / Node.js / Laravel – concept is same)
Pseudo-code:
# /stk-push endpoint (called when user clicks Pay Now)
def stk_push(request):
# Call MPESA STK Push API here...
tx = Transaction.objects.create(status="PENDING")
return JsonResponse({"transaction_id": tx.id})
# /callback (Safaricom will call this)
def callback(request):
data = json.loads(request.body)
tx_id = data["MerchantRequestID"] # example unique ID
result_code = data["ResultCode"]
status = "SUCCESS" if result_code == 0 else "FAILED"
Transaction.objects.filter(id=tx_id).update(status=status)
return JsonResponse({"Result": "OK"})
# /check-status (frontend polls this)
def check_status(request):
tx_id = request.GET.get("tx_id")
tx = Transaction.objects.get(id=tx_id)
return JsonResponse({"status": tx.status})
🌐 Frontend (JavaScript Polling)
<!DOCTYPE html>
<html>
<head>
<title>MPESA STK Push Example</title>
</head>
<body>
<button onclick="initiatePayment()">Pay with MPESA</button>
<p id="statusMsg"></p>
<script>
function initiatePayment() {
// Step 1: Call backend to trigger STK Push
fetch("/stk-push", { method: "POST" })
.then(res => res.json())
.then(data => {
const txId = data.transaction_id;
document.getElementById("statusMsg").innerText = "STK Push sent. Enter your MPESA PIN...";
pollStatus(txId); // start polling
})
.catch(err => console.error("Error:", err));
}
function pollStatus(txId) {
// Step 2: Keep checking every 3s
const interval = setInterval(() => {
fetch(`/check-status?tx_id=${txId}`)
.then(res => res.json())
.then(data => {
console.log("Payment Status:", data.status);
if (data.status !== "PENDING") {
clearInterval(interval);
document.getElementById("statusMsg").innerText = "Payment " + data.status;
}
})
.catch(err => console.error("Error:", err));
}, 3000); // every 3 seconds
}
</script>
</body>
</html>
🔄 What Happens in Practice
- User clicks Pay with MPESA → STK push request sent.
- User enters MPESA PIN on phone.
- Frontend keeps showing “Waiting for confirmation…” and polls backend.
- Safaricom calls your
/callback
with result. - Backend updates DB →
SUCCESS
orFAILED
. - Next poll request sees the new status → frontend updates instantly.
⚠️ Notes for Beginners
- Always use a database (MySQL, Postgres, MongoDB) to store transaction status.
- Don’t keep the frontend waiting forever → you can set a maximum wait time (e.g. 1 minute).
- Polling is simple but not the most efficient → later you can upgrade to WebSockets for real-time push notifications.
✅ Summary
- MPESA STK Push is asynchronous → you don’t get instant results.
- Polling lets your frontend “wait” while the user enters MPESA PIN.
- Use
/stk-push
to initiate,/callback
to receive result, and/check-status
to poll for updates.
Top comments (0)