Handling Callback

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

  1. 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.

  2. STK Push Notification: M-Pesa sends a push notification to the user's phone, prompting them to enter their PIN to authorize the transaction.

  3. Processing: Once the user authorizes the transaction by entering their PIN, M-Pesa processes the payment.

  4. 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.

  5. 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.

.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
MPESA_CALLBACK_URL=https://mywebsite.com/api/mpesa/callback

In our stk push server action, we will modify the callback url parameter

actions/stkPush.ts
// ...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

successful transaction
 {
    "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

successful transaction
{
    "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

api/mpesa/callback.ts
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

folder structure
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ mpesa/
β”‚   β”‚   β”‚   └── [securityKey].ts

Step 2: Add a secret key in .env

.env
MPESA_CALLBACK_SECRET_KEY=anyrandomstring

Step 3: modify your callback url in .env

.env
MPESA_CALLBACK_URL=https://mywebsite.com/api/mpesa/therandomkey

Step 4: Extract the dynamic key and validate it in your callback route

api/mpesa/callback.ts
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 πŸŽ‰!!