Payments
How to Integrate M-Pesa STK Push in a React Native App
The full M-Pesa STK Push flow for a React Native app: access token, payment request, callback, telling the app, the errors you will hit, and going live on Daraja.
By Abdul Rafay··8 min read
M-Pesa is how most people in Kenya pay, so an app for that market needs it. The app side is small; almost all of the work is on your backend. This is the flow used in GlanzGo, a salon booking marketplace in Kenya that Abdul Rafay built as a Software Engineer at Mercury Sols.
How STK Push works
- The user taps Pay, and the app sends the order to your backend.
- Your backend asks Safaricom's Daraja API for an access token.
- Your backend sends an STK Push request with the amount and the customer's phone number.
- The customer gets a prompt on their phone and enters their M-Pesa PIN.
- Safaricom calls your callback URL with the result.
- Your backend marks the order as paid and tells the app.
Rule one: the app never talks to Daraja
The consumer key, consumer secret and passkey stay on your server. Anything inside a mobile app can be extracted. The React Native app only ever calls your own API.
The examples below use Node.js. GlanzGo's backend is Laravel, and the flow is identical.
Step 1: get an access token
const auth = Buffer.from(`${CONSUMER_KEY}:${CONSUMER_SECRET}`).toString("base64");
const res = await fetch(
"https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials",
{ headers: { Authorization: `Basic ${auth}` } }
);
const { access_token } = await res.json();Step 2: send the STK Push request
The password is the shortcode, the passkey and the timestamp joined together and base64-encoded. The same timestamp must be sent in the Timestamp field.
// YYYYMMDDHHmmss
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
const password = Buffer.from(SHORTCODE + PASSKEY + timestamp).toString("base64");
const res = await fetch(
"https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
{
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
BusinessShortCode: SHORTCODE,
Password: password,
Timestamp: timestamp,
TransactionType: "CustomerPayBillOnline",
Amount: 1500,
PartyA: "254712345678", // the customer's number
PartyB: SHORTCODE,
PhoneNumber: "254712345678", // the number that gets the prompt
CallBackURL: "https://api.example.com/payments/mpesa/callback",
AccountReference: "ORDER-1042",
TransactionDesc: "Salon booking",
}),
}
);
// Save this against the order: the callback identifies the payment by it.
const { CheckoutRequestID } = await res.json();Step 3: handle the callback
Safaricom posts the result to your callback URL. A ResultCode of 0 means the customer paid. Anything else means they cancelled, entered the wrong PIN, had too little balance, or the request timed out.
app.post("/payments/mpesa/callback", async (req, res) => {
const callback = req.body.Body.stkCallback;
const paid = callback.ResultCode === 0;
await markOrder(callback.CheckoutRequestID, paid ? "paid" : "failed");
res.json({ ResultCode: 0, ResultDesc: "Accepted" });
});Step 4: tell the app
The app cannot know when the customer has entered their PIN. Show a "Check your phone" screen and either poll your order status endpoint every few seconds or push the result over a WebSocket or a push notification. Give the screen a timeout and a retry button.
Things that go wrong
- Phone number format. Send it in international format without the plus sign, for example 254712345678.
- The callback URL must be a public HTTPS address. Localhost will not work, so use a tunnel during development.
- The callback may never arrive. After a timeout, check the payment with the STK Push Query API (/mpesa/stkpushquery/v1/query).
- The callback may arrive twice. Make the handler safe to run again for the same CheckoutRequestID.
- Never mark an order as paid because the app says so. Only the callback, or the query API, decides.
- Check that the amount in the callback matches the order.
Going live
Everything above runs against the sandbox. To take real payments, apply to go live on the Daraja portal with your paybill or till number, collect your production credentials and passkey, and change the base URL from sandbox.safaricom.co.ke to api.safaricom.co.ke.