Handling Callback
Before handling callback in our donation website, let's first understand what a callback is and why we need it in M-Pesa integration.
What is a Callback and Why We Need it in M-Pesa Integration
A callback also refered as webhooks
is a mechanism through which an external service (in this case, M-Pesa Daraja) notifies your application about the outcome of a transaction or an event. In the context of M-Pesa integration, specifically the STK push process, a callback is crucial because it informs your system about the status of a payment transaction initiated by a user.
How Webhooks Work in the Context of the M-Pesa API STK Integration
-
Transaction Initiation: When a user initiates a payment through your application, your backend sends a request to the M-Pesa API to perform an STK push. This request includes the user's phone number, the amount to be paid, and other necessary details.
-
STK Push Notification: M-Pesa sends a push notification to the user's phone, prompting them to enter their PIN to authorize the transaction.
-
Processing: Once the user authorizes the transaction by entering their PIN, M-Pesa processes the payment.
-
Callback Notification: After processing the transaction, M-Pesa sends a callback to a
predefined URL
on your server. This is the url we provided in the callbackUrl parameter when initiating the stk push. -
Handling the Callback: Your server receives the callback data as a post request, which includes details such as the transaction status (success or failure), transaction CODE, amount paid, and other relevant information. Your server then processes this data to complete your application logic just like how yould normally handle any post request.
Here's a visual representation of the process:
Lets now impliment the callback in our application.
For this, we will need to create an api route in our application folder structure
tech-for-charity/
β
βββ app/
β βββ api/
β β βββ mpesa/
β β β βββ callback.ts
β βββ payments/
β β βββ page.tsx
β βββ layout.tsx
β βββ page.tsx
β
|ββ components/
|ββ actions/
βββ public/
β βββ favicon.ico
β
βββ styles/
β βββ globals.css
β
βββ .gitignore
βββ next.config.js
βββ package.json
βββ .env
βββ README.md
We will now modify our stk body callbackurl parameter. How i like to do it is, add the callback url in the .env and pass it dynamically in the skt push body
Inside our .env
file, we will add the full callback url.
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
MPESA_CALLBACK_URL=https://mywebsite.com/api/mpesa/callback
In our stk push server action, we will modify the callback url parameter
// ...rest of the code
{
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: process.env.MPESA_CALLBACK_URL,
AccountReference: phoneNumber,
TransactionDesc: "anything here",
},
//...rest of the code
Expected Callback body
{
"Body": {
"stkCallback": {
"MerchantRequestID": "12345-67890-12345",
"CheckoutRequestID": "abcdefghijklmnopqrstuvwxyz",
"ResultCode": 0,
"ResultDesc": "The service was accepted successfully",
"CallbackMetadata": {
"Item": [
{
"Name": "Amount",
"Value": 100
},
{
"Name": "MpesaReceiptNumber",
"Value": "ABCDEFGHIJ"
},
{
"Name": "Balance"
"Value": 0
},
{
"Name": "TransactionDate",
"Value": "2023-04-26 12:30:00"
},
{
"Name": "PhoneNumber",
"Value": "254712345678"
}
]
}
}
}
}
failed transaction callback data
{
"Body": {
"stkCallback": {
"MerchantRequestID": "12345-67890-12345",
"CheckoutRequestID": "abcdefghijklmnopqrstuvwxyz",
"ResultCode": 1032,
"ResultDesc": "Request cancelled by user"
}
}
}
Finally, lets write code to handle the callback data on the callback route
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const data = await request.json();
if (!data.Body.stkCallback.CallbackMetadata) {
//for failed transactions
console.log(data.Body.stkCallback.ResultDesc);
return NextResponse.json("ok saf");
}
//lets extract the values from the callback metadata
const body = data.Body.stkCallback.CallbackMetadata
const amountObj = body.Item.find((obj: any) => obj.Name === "Amount");
const amount = amountObj.Value;
//mpesa code
const codeObj = body.Item.find(
(obj: any) => obj.Name === "MpesaReceiptNumber"
);
const mpesaCode = codeObj.Value;
//phone number - in recent implimentations, it is hashed.
const phoneNumberObj = body.Item.find(
(obj: any) => obj.Name === "PhoneNumber"
);
const phoneNumber = phoneNumberObj.Value.toString();
try {
//complete your logic - Eg saving transaction to db
console.log({amount, mpesaCode, phoneNumber})
return NextResponse.json("ok", { status: 200 });
} catch (error: any) {
return NextResponse.json("ok");
}
}
If youve noticed, i am sending a response back to safaricom. These is important to avoid double hitting or our endpoint since if a response delays, mpesa api assumes it as failed and retries, which can cause double execution of your code if not well handled.
Any logic written in this route is not specific to mpesa intergration. You can impliment this route however you would wish.
Callback URL Security
In this section, we will explore how to secure your callback URL to prevent exposing your callback route logic to malicious individuals.
The code written in our callback route handles a specific body structure. If someone gains knowledge of your callback URL path, they could send a body identical to the one sent by the Mpesa API and have it handled as an authentic request.
How can we counter this? While a system cannot be 100% secure, I will share some measures you can use to stay cautious.
1. Using Dynamic Callback Routes
By using dynamic callback routes, we can create a security key, save it in an .env
file, and attach it to the callback URL. This way, we can validate the dynamic key since only the developer has access to it in the .env
file. We will ignore any request whose dynamic key is not the one in our .env
file.
Implementation
Step 1: Make your callback route a dynamic route
β βββ api/
β β βββ mpesa/
β β β βββ [securityKey].ts
Step 2: Add a secret key in .env
MPESA_CALLBACK_SECRET_KEY=anyrandomstring
Step 3: modify your callback url in .env
MPESA_CALLBACK_URL=https://mywebsite.com/api/mpesa/therandomkey
Step 4: Extract the dynamic key and validate it in your callback route
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const data = await request.json();
const { securityKey } = request.params;
if (securityKey !== process.env.MPESA_CALLBACK_SECRET_KEY) {
// ignore the requets
return NextResponse.json("ok saf");
}
//...rest of the code
}
2. IP Whitelisting
We can add an extra security layer by whitelisting a list of IPs provided by Safaricom from which valid requests will be coming.
Here is the list of the IP addresses provided by Safaricom in the Mpesa documentation:
196.201.214.200
196.201.214.206
196.201.213.114
196.201.214.207
196.201.214.208
196.201.213.44
196.201.212.127
196.201.212.138
196.201.212.129
196.201.212.136
196.201.212.74
196.201.212.69
Implementation
Step 1: Extract the IP address from the incoming request
You can extract the IP address from the request header.
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('remote-addr');
const whitelist = [
'196.201.214.200',
'196.201.214.206',
'196.201.213.114',
'196.201.214.207',
'196.201.214.208',
'196.201.213.44',
'196.201.212.127',
'196.201.212.138',
'196.201.212.129',
'196.201.212.136',
'196.201.212.74',
'196.201.212.69'
];
if (!whitelist.includes(clientIp)) {
return NextResponse.json({ error: 'IP not whitelisted' }, { status: 403 });
}
// Rest of the code
}
Happy Coding π!!