How to Integrate the Mpesa Express API in Flask and React JS real world project.

Daudi MwanziaDaudi Mwanzia
13 min read

Use Mpesa Express in a simple full stack Flask and React application.

Introduction

In this blog article, we are going to be creating API routes in Flask using the Lipa na Mpesa API and consuming the APIs in React JS frontend. The article will demonstrate a practical application of Lipa na Mpesa in a simple full stack application, where a user is presented with products and a cart where they can proceed to checkout, where a prompt(STK Push) is send to a user for payment, payment details stored and success or fail is shown to the user, also demonstrating a practical linkage of Lipa na Mpesa API to an order, as is in a online checkout system where a user makes a payment for a certain products.

In our backend, we are going to create a Product table, Payment Table, a Cart Table, where a Cart can have multiple products, and an Order table where an order can contain multiple products, and where a payment record is going to be associated with an order and the associated flask routes.

In our frontend, we are going to create a react component that displays products that can be selected and added to cart, where a user can then add their Mpesa number and proceed to payment.

Prerequisites

  1. Safaricom developer account:

    Create a developer account here and go to MY APPS and create a new app as below.

  2. Basic Flask knowledge and knowledge on installing Python Flask Framework and using pipenv for package installations creating an python environment.

  3. Knowledge on installing and using React JS using vite.

  4. Knowledge on APIs.

Payment Logic

A user is presented with products that can be added to a cart, where quantity can be increased by adding to cart again.

The user provides their phone number and proceeds to check out.

For payment, we will have two routes, one to trigger an STK push by sending the phone number and amount to the API, and a callback route to process the transactions, create an order and payment records and update cart payment status.

For successful payment, the following logic is used in our backend. By proceeding to checkout, the current cart id is send to the to the callback route through the trigger route where the API sends the transaction information. In the trigger route, we are sending a callback function(hosted online) in our payload along other items such as amount, phone number and till number.

The callback route extracts the details from the response by the Mpesa Express API, initiates an order creation using the current cart items. It also associates the newly created order with the new payment we are making, by creating the payment record. An order and payment record can only be created when payment is successful ensuring that an order is ready for shipping if its creation is successful.

Understanding the Lipa na MPesa API

After creating an app in the developer dashboard, navigate to the APIS tab and select stimulate in the Mpesa Express container, this will help us get the credentials and information we need, and to also test the STK Push from the developer dashboard. See below.

Change the phone number to your phone number for testing purposes without changing the till number which is provided by Safaricom for testing purposes and click on simulate request. See below.

This is going to send an STK push to your number and take you to the next page which is a console where you can see different implementations of a request and the expected response in different programming languages. We are more interested in the python Implementation.

Our main interest in the code above which is a script for sending a POST request to a Lipa na Mpesa endpoint which takes the headers, and the payload as specified by the Lipa na Mpesa API, is the following lines. In our payload, we have a callback key and value pair which represents the endpoint where the Mpesa API will send the Transaction details such as Transaction code and amount, we will update the CallBackURL to match our hosted callback function or route.

The expected data from the transaction if it is successful is also shown below.

"CallBackURL": "https://mydomain.com/path",

response = requests.request("POST", 'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest', headers = headers, data = payload)
Received data: {'Body': {'stkCallback': {'MerchantRequestID': '758b-43a9-a2f9-456760d661b3302111', 'CheckoutRequestID': 'ws_CO_01042024151639180790926481', 'ResultCode': 0, 'ResultDesc': 'The service request is processed successfully.', 'CallbackMetadata': {'Item': [{'Name': 'Amount', 'Value': 1.0}, {'Name': 'MpesaReceiptNumber', 'Value': 'SD14SWH0T8'}, {'Name': 'Balance'}, {'Name': 'TransactionDate', 'Value': 20240401151409}, {'Name': 'PhoneNumber', 'Value': 254790926481}]}}}}

We are now going to change the python script by Daraja API, into a Flask format by making two endpoints, one to trigger the STK push and the other to print out transaction Details, which is the callback route.

Here is how we can do it:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

@app.route('/trigger', methods=['POST'])
def trigger_request():
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer kSd6QZvn82EGYoSgNKzAaEbCTArD'
    }

    payload = {
        "BusinessShortCode": 174379,
        "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjQwMzI2MDAwMzU2",
        "Timestamp": "20240326000356",
        "TransactionType": "CustomerPayBillOnline",
        "Amount": 1,
        "PartyA": 254708374149,
        "PartyB": 174379,
        "PhoneNumber": 254708374149,
        "CallBackURL": "https://mydomain.com/path",
        "AccountReference": "CompanyXLTD",
        "TransactionDesc": "Payment of X"
    }
    response = requests.request("POST", 'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest', headers=headers, data=payload)
    return response.text.encode('utf8')

@app.route('/callback', methods=['POST'])
def callback_handler():
    data = request.get_json()
    print(data)
    return jsonify({'success': True})

if __name__ == '__main__':
    app.run(debug=True)

Your payload and Authorization header will be different in your case. Remember to change the the necessary items.

What we have done above it to create two Flask routes which we can use in our react application.
The first route will be used to trigger an STK Push, where a User can enter their Safaricom phone number, and the callback route will be used to print transaction details, but we do not want to just print them in our console, we have to make use of the details to persist data in our database, which we will see. We are now going to see the Models we will need for the full stack application, and the routes associated with them.

Product Model, Order Model, Cart Model, Payment model and associated routes.

To match our use case, we will created the following Models, and make the associated routes which include Get products, Get order, and Get Transaction details, Add to cart and so on. After this we are going to associate our callback route to an cart id, which will help us create a new order and a new Payment record associated with that order. We will do this using a URL parameter in the CallBackURL as we will see.

This are the models, and full code for models can be found here:

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    price = db.Column(db.Float, nullable=False)
    order_items = db.relationship("OrderItem", backref="product", lazy=True)
    cart_items = db.relationship("CartItem", backref="cart_product", lazy=True)

    def __repr__(self):
        return f"Product(name={self.name}, price={self.price})"


class Order(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    order_items = db.relationship("OrderItem", backref="order", lazy=True)
    payment = db.relationship("Payment", backref="order", uselist=False)

    def __repr__(self):
        return f"Order(id={self.id})"


class OrderItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
    product_id = db.Column(db.Integer, db.ForeignKey("product.id"), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)

    def __repr__(self):
        return f"OrderItem(order_id={self.order_id}, product_id={self.product_id}, quantity={self.quantity})"


class Payment(db.Model):
    __tablename__ = "payment"
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
    payment_amount = db.Column(db.Float)
    payment_date = db.Column(db.DateTime)
    payment_method = db.Column(db.String)
    status = db.Column(db.String)
    transaction_id = db.Column(db.String)

    @validates("payment_amount")
    def validate_payment_amount(self, key, payment_amount):
        if payment_amount < 0:
            raise ValueError("Payment amount cannot be negative")
        return payment_amount


class Cart(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    status = db.Column(db.String(50), default="pending")
    cart_items = db.relationship("CartItem", backref="cart", lazy=True)

    def __repr__(self):
        return f"Cart(id={self.id})"


class CartItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    cart_id = db.Column(db.Integer, db.ForeignKey("cart.id"), nullable=False)
    product_id = db.Column(db.Integer, db.ForeignKey("product.id"), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    product = db.relationship(
        "Product", backref=db.backref("cart_items_rel", lazy="dynamic")
    )

    def __repr__(self):
        return f"CartItem(cart_id={self.cart_id}, product_id={self.product_id}, quantity={self.quantity})"

The routes we interested in regards to payment are as shown below, find complete code here. This includes the get token route which gets an authorization token from the Daraja API to enable the STK Push, the trigger route which receives the phone number and current cart id which is send as a URL parameter in the CallBackURL. Other routes we are interested in are the callback route which creates the order and payment records, the poll cart status used to check if a cart has been paid for so as to show payment success in our react app.

@app.route("/get_token")
def get_access_token():
    consumer_key = CONSUMER_KEY
    consumer_secret = CONSUMER_SECRET
    access_token_url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
    headers = {"Content-Type": "application/json"}
    auth = (consumer_key, consumer_secret)
    try:
        response = requests.get(access_token_url, headers=headers, auth=auth)
        response.raise_for_status()  # Raise exception for non-2xx status codes
        result = response.json()
        access_token = result["access_token"]
        return jsonify({"access_token": access_token})
    except requests.exceptions.RequestException as e:
        return jsonify({"error": str(e)})


@app.route("/trigger", methods=["POST"])
def trigger_request():
    data = request.get_json()
    cart_id = data.get("cart_id")
    phone_number = data.get("phone_number")
    access_token = data.get("access_token")
    callBackURL = f"https://86lse6-ip-154-159-237-192.tunnelmole.net/callback/{cart_id}"

    # Retrieve the cart items associated with the provided cart_id
    cart_items = CartItem.query.filter_by(cart_id=cart_id).all()

    # Initialize total amount to 0
    total_amount = 0

    # Iterate over cart items and sum up the total amount
    for cart_item in cart_items:
        # Retrieve the associated product for the cart item
        product = cart_item.product

        # Access the price of the product and multiply by the quantity
        product_price = product.price
        quantity = cart_item.quantity
        item_total = product_price * quantity

        # Add the item total to the total amount
        total_amount += item_total
    print("Total Amount:", total_amount)
    total_amount = int(total_amount)

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {access_token}",
    }

    payload = {
        "BusinessShortCode": 174379,
        "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjQwNDA1MDgxOTEx",
        "Timestamp": "20240405081911",
        "TransactionType": "CustomerPayBillOnline",
        "Amount": 1,
        "PartyA": phone_number,
        "PartyB": 174379,
        "PhoneNumber": phone_number,
        "CallBackURL": callBackURL,
        "AccountReference": "CompanyXLTD",
        "TransactionDesc": "Payment of X",
    }

    response = requests.request(
        "POST",
        "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
        headers=headers,
        json=payload,
    )
    return response.text.encode("utf8")


def create_order_from_cart(cart):
    if not cart:
        return jsonify({"error": "Cart not found"}), 404

    # Create an order using the cart items
    order = Order()
    db.session.add(order)
    db.session.flush()  # Flush to get the order ID before adding order items

    for cart_item in cart.cart_items:
        order_item = OrderItem(
            order_id=order.id,
            product_id=cart_item.product_id,
            quantity=cart_item.quantity,
        )
        db.session.add(order_item)

    # Commit changes to the database
    db.session.commit()

    return order.id


@app.route("/callback/<int:cart_id>", methods=["POST"])
def callback_handler(cart_id):
    data = request.get_json()

    # Debugging: Print received data
    print("Received data:", data)

    # Extract the relevant data from the callback
    items = data["Body"]["stkCallback"]["CallbackMetadata"]["Item"]
    extracted_data = {item["Name"]: item.get("Value", None) for item in items}

    # Debugging: Print extracted data
    print("Extracted data:", extracted_data)

    mpesa_receipt_number = extracted_data.get("MpesaReceiptNumber")
    payment_amount = extracted_data.get("Amount")
    transaction_date_str = str(extracted_data.get("TransactionDate"))
    transaction_date = datetime.strptime(transaction_date_str, "%Y%m%d%H%M%S")

    # Debugging: Print extracted payment details
    print("Mpesa Receipt Number:", mpesa_receipt_number)
    print("Payment Amount:", payment_amount)
    print("Transaction Date:", transaction_date)

    # Find the cart associated with the cart_id
    cart = Cart.query.get(cart_id)
    if not cart:
        return jsonify({"error": "Cart not found"}), 404

    # Create an order using the cart items
    order_id = create_order_from_cart(cart)
    order = Order.query.get(order_id)

    # Debugging: Print created order details
    print("Created Order:", order)

    # Create a new Payment record associated with the order
    payment = Payment(
        order_id=order.id,
        payment_amount=float(payment_amount),
        payment_date=transaction_date,
        payment_method="mpesa",
        status="paid",
        transaction_id=mpesa_receipt_number,
    )

    # Debugging: Print created payment details
    print("Created Payment:", payment)

    # Add the payment to the database
    db.session.add(payment)
    db.session.commit()

    # Update the cart status to "paid"
    cart.status = "paid"
    db.session.add(cart)
    db.session.commit()

    return jsonify({"success": True, "order_id": order.id})


@app.route("/poll_cart_status/<int:cart_id>", methods=["GET"])
def handle_poll_cart_status(cart_id):
    cart = Cart.query.get(cart_id)
    if not cart:
        return jsonify({"error": "Cart not found"}), 404
    return jsonify({"status": cart.status})

The trigger route takes in a cart_id, the phone_number and calculates the total amount based on the cart items(amount is set to 1 for testing). As mentioned before, we are going to send some data to the API, such as phone number and amount, and a callback URL though the trigger route. The cart_id is send through the callback URL as a path/URL parameter as shown here, "/callback/{cart_id}", this will help us create an order by going through items selected by user and create a payment associated to that order in the callback route. By providing the cart id, we associate the route that receives payment details to a certain cart which implies a certain order that can further be used, for say shipping. A user pays for that order.

To be able to use our callback route as the callback URL for our trigger route, we need to expose our route to the internet(https). We can do this by hosting our application, or by using a tunneling service such as Ngrok or tunnelmole, as localhost(http) is not an option, the API cannot forward the response to the local server.

The callback route above takes in a URL parameter, cart_id and also receives the Transaction details from the Lipa na Mpesa API. It extracts the data received which includes Mpesa Code, Amount, Date among other details which will be used to create a payment record. The route, using the cart_id queries the database for cart items which are the selected products we will use to create the order, and creates a payment associating it with the newly created order. The callback also sets the status of the cart to paid when payment is successful. This will help us know if the cart is paid for, which will in turn be used to update user that the payment is successful. The user can now be presented with a payment successful message.

React Frontend

Based on the flask routes, which can be found here, we can use the following logic to create a simple React component that uses the routes.

First, we will present the user with a list of products, each with a button that can be clicked to add the product to the cart, where clicking it multiple times adds the quantity of the item. Our simple application can only be used by one user, so we have only one cart, this can be changed by associating the cart with a certain logged in user.

A user is also presented with cart items, where they can remove a product from cart, where a user can finally proceed to checkout, where they will provide their valid Mpesa number, a success page is then shown upon successful payment.

Full code for the react component can be found here, but what we are interested in regards to payment are the following lines of code. As we saw in our callback route, we need the cart_id, this is being send from the react application in the following code which sends the cart_id to the trigger route, which in turn sends it as part of the callback URL.

We can also see our handleCheckout function which does a number of things including getting the token, which is send through to the trigger route, and lastly, after checkout, we need to start polling the cart status to see if the current cart has been paid for, where we can show the success of fail message.

  useEffect(() => {
    // Fetch the current cart ID from the server
    fetch("http://127.0.0.1:5000/current_cart")
      .then((response) => response.json())
      .then((data) => {
        if (data.error) {
          console.error("Error fetching cart ID:", data.error);
        } else {
          setCartId(data.cart_id);
          // Fetch cart items
          fetchCartItems(data.cart_id);
        }
      })
      .catch((error) => console.error("Error fetching cart ID:", error));

    // Fetch products
    fetchProducts();
  }, []);

  const handleCheckout = () => {
    if (phoneNumber.trim() === "") {
      alert("Please enter your phone number");
      return;
    }

    if (!cartId) {
      console.error("Cart ID not found");
      return;
    }

    // Fetch the token
    fetch("http://127.0.0.1:5000/get_token")
      .then((response) => {
        if (!response.ok) {
          throw new Error("Failed to fetch access token");
        }
        return response.json();
      })
      .then((data) => {
        const accessToken = data.access_token;

        // Send trigger request with the obtained access tokend
        fetch("http://127.0.0.1:5000/trigger", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            phone_number: phoneNumber,
            cart_id: cartId,
            access_token: access_token, // Send the access token as a parameter
          }),
        })
          .then((response) => {
            if (!response.ok) {
              throw new Error("Failed to initiate payment");
            }
            return response.text();
          })
          .then((data) => {
            console.log("Payment initiated:", data);
            alert("Payment initiated successfully");
          })
          .catch((error) => {
            console.error("Error initiating payment:", error);
            alert("Failed to initiate payment");
          });
      })
      .catch((error) => {
        console.error("Error fetching access token:", error);
        alert("Failed to fetch access token");
      });
  };
  const startPollingPaymentStatus = () => {
    const interval = setInterval(() => {
      fetch(`http://127.0.0.1:5000/poll_cart_status/${cartId}`)
        .then((response) => response.json())
        .then((data) => {
          if (data.status === "paid") {
            setPaymentStatus("paid");
            clearInterval(interval);
          } else if (data.status === "failed") {
            setPaymentStatus("failed");
            clearInterval(interval);
          }
        })
        .catch((err) => {
          console.error("Error checking payment status:", err);
          setPaymentStatus("error");
          clearInterval(interval);
        });
    }, 5000);
  };

Our Application will look like this.

Payment Success Page will look like this:

Error handling can be handles in the callback in case of payment failure, but in our case, we give the user 15 seconds to pay for the transaction, which otherwise means payment failure.

Conclusion

The aim of this article was to associate Mpesa Express API with a real world project which we have covered. Find the code for the whole project in my Github here.

2
Subscribe to my newsletter

Read articles from Daudi Mwanzia directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Daudi Mwanzia
Daudi Mwanzia