Server Setup

Setting Up the Backend Logic

Lets modify the folder structure to add .env file to store mpesa credentials and add an actions folder where we will keep our server actions logic

Here is the updated folder structure

tech-for-charity/
β”‚
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ payments/
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
β”‚
|── components/
|── actions/
β”œβ”€β”€ public/
β”‚   └── favicon.ico
β”‚
β”œβ”€β”€ styles/
β”‚   β”œβ”€β”€ globals.css
β”‚
β”œβ”€β”€ .gitignore
β”œβ”€β”€ next.config.js
β”œβ”€β”€ package.json
β”œβ”€β”€ .env
└── README.md

Make sure the .env file is added to .gitignore file to avoid sending your credentials to version control.

Inside our .env file, we will add the keys we got from the Going Live section.

.env
MPESA_CONSUMER_KEY= your_mpesa_consumer_key
MPESA_CONSUMER_SECRET=your_mpesa_secret_key
MPESA_PASSKEY=your_mpesa_passkey
MPESA_SHORTCODE= 123456 //store number for till numbers
MPESA_ENVIRONMENT=your_mpesa_environment //live or sandbox

We will add some keys to this file later on as we look into how to secure your mpesa callback endpoints

Creating the server actions

Before we create the server actions to make calls to the daraja api, lets understand the daraja Api structure as this is crucial part of our interaction with the API.

Here is how it works, we will utilize three apis

  1. Authorization API

    • This API allows us to authenticate and authorize all our API calls to Daraja.
    • Endpoint: MPESA_BASE_URL/oauth/v1/generate?grant_type=client_credentials
  2. Mpesa Express API

    • This API allows us to initiate the STK push.
    • Endpoint: MPESA_BASE_URL/mpesa/stkpush/v1/processrequest
  3. STK Query API

    • This API allows us to track the real-time status of the STK push interaction from the user.
    • Endpoint: MPESA_BASE_URL/mpesa/stkpushquery/v1/query

The base Url is different depending on your mpesa credentials MPESA_ENVIRONMENT

MPESA_BASE_URL
  // sandbox
  const MPESA_BASE_URL =  "https://sandbox.safaricom.co.ke"
 
  //live
  const MPESA_BASE_URL =  "https://api.safaricom.co.ke"
 

stk push server action

Meanwhile, lets install axios to help us in making api requests.

Terminal
npm install axios

Inside the actions folder, create a stkPush.ts file and add the following content

actions/stkPush.ts
"use server";
 
import axios from "axios";
 
interface Params {
  mpesa_number: string;
  name: string;
  amount: number;
}
 
export const sendStkPush = async (body: Params) => {
  const mpesaEnv = process.env.MPESA_ENVIRONMENT;
  const MPESA_BASE_URL =
    mpesaEnv === "live"
      ? "https://api.safaricom.co.ke"
      : "https://sandbox.safaricom.co.ke";
 
  const { mpesa_number: phoneNumber, name, amount } = body;
  try {
    //generate authorization token
    const auth: string = Buffer.from(
      `${process.env.MPESA_CONSUMER_KEY}:${process.env.MPESA_CONSUMER_SECRET}`
    ).toString("base64");
 
    const resp = await axios.get(
      `${MPESA_BASE_URL}/oauth/v1/generate?grant_type=client_credentials`,
      {
        headers: {
          authorization: `Basic ${auth}`,
        },
      }
    );
 
    const token = resp.data.access_token;
 
    const cleanedNumber = phoneNumber.replace(/\D/g, "");
 
    const formattedPhone = `254${cleanedNumber.slice(-9)}`;
 
    const date = new Date();
    const timestamp =
      date.getFullYear() +
      ("0" + (date.getMonth() + 1)).slice(-2) +
      ("0" + date.getDate()).slice(-2) +
      ("0" + date.getHours()).slice(-2) +
      ("0" + date.getMinutes()).slice(-2) +
      ("0" + date.getSeconds()).slice(-2);
 
    const password: string = Buffer.from(
      process.env.MPESA_SHORTCODE! + process.env.MPESA_PASSKEY + timestamp
    ).toString("base64");
 
    const response = await axios.post(
      `${MPESA_BASE_URL}/mpesa/stkpush/v1/processrequest`,
      {
        BusinessShortCode: process.env.MPESA_SHORTCODE,
        Password: password,
        Timestamp: timestamp,
        TransactionType: "CustomerPayBillOnline", //CustomerBuyGoodsOnline - for till
        Amount: amount,
        PartyA: formattedPhone,
        PartyB: process.env.MPESA_SHORTCODE, //till number for tills
        PhoneNumber: formattedPhone,
        CallBackURL: "https://mydomain.com/callback-url-path",
        AccountReference: phoneNumber,
        TransactionDesc: "anything here",
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );
    return { data: response.data };
  } catch (error) {
    if (error instanceof Error) {
      console.log(error);
      return { error: error.message };
    }
    return { error: "something wrong happened" };
  }
};

Inside the same actions, also create a stkPushQuery.ts file and add the following content - we will make use of this later on

actions/stkPushQuery.ts
"use server";
 
import axios from "axios";
 
export const stkPushQuery = async (reqId: string) => {
  const mpesaEnv = process.env.MPESA_ENVIRONMENT;
  const MPESA_BASE_URL =
    mpesaEnv === "live"
      ? "https://api.safaricom.co.ke"
      : "https://sandbox.safaricom.co.ke";
 
  try {
    //generate token
    const auth: string = Buffer.from(
      `${process.env.MPESA_CONSUMER_KEY}:${process.env.MPESA_CONSUMER_SECRET}`
    ).toString("base64");
 
    const resp = await axios.get(
      `${MPESA_BASE_URL}/oauth/v1/generate?grant_type=client_credentials`,
      {
        headers: {
          authorization: `Basic ${auth}`,
        },
      }
    );
 
    const token = resp.data.access_token;
 
    const date = new Date();
    const timestamp =
      date.getFullYear() +
      ("0" + (date.getMonth() + 1)).slice(-2) +
      ("0" + date.getDate()).slice(-2) +
      ("0" + date.getHours()).slice(-2) +
      ("0" + date.getMinutes()).slice(-2) +
      ("0" + date.getSeconds()).slice(-2);
 
    const password: string = Buffer.from(
      process.env.MPESA_SHORTCODE! + process.env.MPESA_PASSKEY + timestamp
    ).toString("base64");
 
    const response = await axios.post(
      `${MPESA_BASE_URL}/mpesa/stkpushquery/v1/query`,
      {
        BusinessShortCode: process.env.MPESA_SHORTCODE,
        Password: password,
        Timestamp: timestamp,
        CheckoutRequestID: reqId,
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );
    return { data: response.data };
  } catch (error) {
    if (error instanceof Error) {
      return { error: error };
    }
 
    const unknownError = error as any;
    unknownError.message = "something wrong happened";
    return { error: unknownError };
  }
};