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.
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
-
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
-
Mpesa Express API
- This API allows us to initiate the STK push.
- Endpoint:
MPESA_BASE_URL/mpesa/stkpush/v1/processrequest
-
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
// 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.
npm install axios
Inside the actions folder, create a stkPush.ts
file and add the following content
"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
"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 };
}
};