Mark Mwangi logoMark
Mwangi
Return to home
Get in
touch
Admin
login
Payment IntegrationJune 25, 202613 min read

Implementing M-Pesa payment gateway in a web application

A step-by-step guide to integrating the M-Pesa Daraja API — STK push flow, callbacks, error handling, and security.

By Mark Mwangi
Implementing M-Pesa payment gateway in a web application cover coverImage

M-Pesa is the dominant mobile money platform in Kenya and across much of Africa. Integrating it lets your web app accept payments directly from a customer's phone. This guide walks through the Safaricom Daraja API, the STK push flow, and the parts people get wrong.

1. Prerequisites

  • A **Safaricom Daraja** account (developer sandbox + production).
  • **Consumer key** and **consumer secret** from your app.
  • A **shortcode** (paybill or till number).
  • A **publicly reachable callback URL** for payment results.

2. Get an access token

Tokens are short-lived. Fetch one before each payment (or cache briefly):

bash
curl -X GET   "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"   -H "Authorization: Basic $(echo -n '$KEY:$SECRET' | base64)"

3. Initiate STK push (Lipa Na M-Pesa)

This prompts the customer's phone to enter their PIN.

ts
const res = await fetch(
  "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      BusinessShortCode: "174379",
      Password: password,            // base64(shortcode+passkey+timestamp)
      Timestamp: timestamp,
      TransactionType: "CustomerPayBillOnline",
      Amount: 100,
      PartyA: "254712345678",
      PartyB: "174379",
      PhoneNumber: "254712345678",
      CallBackURL: "https://yourapp.com/api/mpesa/callback",
      AccountReference: "ORDER-123",
      TransactionDesc: "Payment for order",
    }),
  }
);

4. Handle the callback

Safaricom calls your CallBackURL with the result. Store it and reconcile with the order:

ts
export async function POST(req: Request) {
  const body = await req.json();
  const result = body.Body.stkCallback;
  const success = result.ResultCode === 0;
  await updateOrder(result.CheckoutRequestID, {
    status: success ? "paid" : "failed",
    receipt: result.CallbackMetadata?.ReceiptNumber,
  });
  return Response.json({ ok: true });
}

5. Error handling and edge cases

  • **Timeout**: the customer never enters a PIN — poll the transaction status API.
  • **Duplicate**: guard against the callback firing twice (idempotency by `CheckoutRequestID`).
  • **Sandbox vs production**: different base URLs and shortcodes.

6. Security

  • Keep credentials in environment variables; never ship the passkey to the client.
  • Verify the callback came from Safaricom (IP allowlist or shared secret).
  • Don't trust the callback alone — confirm via the transaction status API for high-value orders.

Checklist

  • [ ] App registered on Daraja
  • [ ] Access token fetched and cached
  • [ ] STK push sends to the right phone
  • [ ] Callback persists and reconciles the order
  • [ ] Timeout/retry path implemented

M-Pesa integration is mostly about getting the STK push right and treating the callback as untrusted input. Do those two well and the rest follows.

Payment IntegrationDevelopmentBest Practices
Go back to all blogs →Go to all projects →Contact →

Related Articles

  • The importance of documentation in software development
  • Scalable architecture patterns for modern web applications
  • types of API

About

Mark Mwangi

Creating secure and meaningful digital experiences.

Software developer focused on secure, responsive, and high-quality digital experiences.

Socials

GitHubLinkedInXE-mailinstagramfacebookwhatsappMedium

© 2026 Mark Mwangi | All rights reserved

Terms of Service•Privacy Policy

Last updated: April 30, 2026 16:23:51 UTC