DEV Community

Joram Wambugu
Joram Wambugu

Posted on • Updated on

MPesa APIs Integration in Go

What is MPesa?
Mpesa is a mobile banking service launched by Safaricom, the largest mobile phone network in Kenya, in 2007 that allows users to send or save money through their mobile phones.

Since the service is so much popular, Safaricom introduced a REST API that allows developers to integrate their apps with MPesa.

Requirements

  • Go installed on your local computer.
  • Postman installed locally since we'll use it to test the APIs.

The completed code can be found here.

What we will cover

Creating a developer's account

In order to start consuming the APIs, you will need an account. To create one, please go to Daraja and click on the Login/Sign Up button then choose Go to Sign Up

Please fill all the fields and select Individual for the Account Type since we will be using it for learning purposes. Once done, a confirmation email will be sent, and you can now access the portal.

Creating an app

All the MPesa APIs require a developer to have an app so let's create one.

  • Step 1: Click on MY APPS tab
  • Step 2: Click on CREATE NEW APP button
  • Step 3: Fill in the App Name and select Lipa Na MPesa Sandbox and Mpesa Sandbox products
  • Step 4: Finish by clicking CREATE APP button.

Once done, you will see the newly added app on one of the cards with its name, consumer key and consumer secret.

Project Set Up

Create a new project then open it using your favorite IDE or editor then run the command below to initialize new module in current directory.

go mod init mpesa-golang
Enter fullscreen mode Exit fullscreen mode

Once done, create a new file main.go and add the snippet below.

This project is for learning purposes. You should not store any credentials on your code

package main

import (
    "io"
    "net/http"
    "time"
)

// Mpesa is an application that will be making a transaction
type Mpesa struct {
    consumerKey    string
    consumerSecret string
    baseURL        string
    client         *http.Client
}

// MpesaOpts stores all the configuration keys we need to set up a Mpesa app,
type MpesaOpts struct {
    ConsumerKey    string
    ConsumerSecret string
    BaseURL        string
}

// NewMpesa sets up and returns an instance of Mpesa
func NewMpesa(m *MpesaOpts) *Mpesa {
    client := &http.Client{
        Timeout: 30 * time.Second,
    }

  return &Mpesa{
      consumerKey:    m.ConsumerKey,
      consumerSecret: m.ConsumerSecret,
      baseURL:        m.BaseURL,
      client:         client,
  }
}

// makeRequest performs all the http requests for the specific app
func (m *Mpesa) makeRequest(req *http.Request) ([]byte, error) {
    resp, err := m.client.Do(req)
    if err != nil {
      return nil, err
    }

    defer func(Body io.ReadCloser) {
        _ = Body.Close()
    }(resp.Body)

    body, err := io.ReadAll(resp.Body)
    if err != nil {
       return nil, err
    }

    return body, nil
}
Enter fullscreen mode Exit fullscreen mode
  • The Mpesa struct will store the details about an app and the base url to be used since it changes based on the environment. The fields are unexported since we will only be using them within the package.

  • The NewMpesa function is a constructor that we'll set up and return an instance of Mpesa.

  • makeRequest will handle the sending of http requests for the app.

Generating Access Token

In order to interact with the MPesa APIs, we will need to provide an access token to authenticate our requests which is generated using Consumer Key and Consumer Secret generated when creating a new app.

To get these credentials, click on MY APPS tab to list all your apps and copy the Consumer Key and Consumer Secret keys for the app we created here.

In order to generate the access token, we will add a new struct to handle the processing of the response so add the snippet below:

// MpesaAccessTokenResponse is the response sent back by Safaricom when we make a request to generate a token
type MpesaAccessTokenResponse struct {
    AccessToken  string `json:"access_token"`
    ExpiresIn    string `json:"expires_in"`
    RequestID    string `json:"requestId"`
    ErrorCode    string `json:"errorCode"`
    ErrorMessage string `json:"errorMessage"`
}
Enter fullscreen mode Exit fullscreen mode

The above fields found can be found here on the developer's portal. We will then create a new method generateAccessToken for generating the access token.

// generateAccessToken sends a http request to generate new access token
func (m *Mpesa) generateAccessToken() (*MpesaAccessTokenResponse, error) {
    url := fmt.Sprintf("%s/oauth/v1/generate?grant_type=client_credentials", m.baseURL)

    req, err := http.NewRequest(http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    req.SetBasicAuth(m.consumerKey, m.consumerSecret)
    req.Header.Set("Content-Type", "application/json")

    resp, err := m.makeRequest(req)
    if err != nil {
        return nil, err
    }

    accessTokenResponse := new(MpesaAccessTokenResponse)
    if err := json.Unmarshal(resp, &accessTokenResponse); err != nil {
        return nil, err
    }

    return accessTokenResponse, nil
}
Enter fullscreen mode Exit fullscreen mode
  • The above method creates a new http request then set uses SetBasicAuth method to set the request's Authorization header to use HTTP Basic Authentication with the provided consumer_key and consumer_secret.
  • We then make a http request and unmarshal the response to MpesaAccessTokenResponse struct.

We can then test if we can be able to generate an access token by adding the following block to our main method.

func main() {
    mpesa := NewMpesa(&MpesaOpts{
        ConsumerKey:    "your-consumer-key-goes-here",
        ConsumerSecret: "your-consumer-secret-goes-here",
        BaseURL:        "https://sandbox.safaricom.co.ke",
    })

    accessTokenResponse, err := mpesa.generateAccessToken()
    if err != nil {
        log.Fatalln(err)
    }

    fmt.Printf("%+v\n", accessTokenResponse)
}
Enter fullscreen mode Exit fullscreen mode

If the credentials are valid, you should get a response similar to this one.

  &{AccessToken:TchBwjZgTIq17dVbi3SRo2OsWvNB ExpiresIn:3599 RequestID: ErrorCode: ErrorMessage:}
Enter fullscreen mode Exit fullscreen mode

If you are having any issues, you can refer to the documentation
for more details.

Integrating Lipa na Mpesa

Lipa na M-Pesa Online Payment API is used to initiate an MPesa transaction on behalf of a customer using STK Push. This API simplifies the checkout process since the customer doesn't need to enter the account number or the paybill (shortcode) they're paying to.

In order to make the request, we will add two new structs as follows:

// STKPushRequestBody is the body with the parameters to be used to initiate an STK push request
type STKPushRequestBody struct {
    BusinessShortCode string `json:"BusinessShortCode"`
    Password          string `json:"Password"`
    Timestamp         string `json:"Timestamp"`
    TransactionType   string `json:"TransactionType"`
    Amount            string `json:"Amount"`
    PartyA            string `json:"PartyA"`
    PartyB            string `json:"PartyB"`
    PhoneNumber       string `json:"PhoneNumber"`
    CallBackURL       string `json:"CallBackURL"`
    AccountReference  string `json:"AccountReference"`
    TransactionDesc   string `json:"TransactionDesc"`
}

// STKPushRequestResponse is the response sent back after initiating an STK push request.
type STKPushRequestResponse struct {
    MerchantRequestID   string `json:"MerchantRequestID"`
    CheckoutRequestID   string `json:"CheckoutRequestID"`
    ResponseCode        string `json:"ResponseCode"`
    ResponseDescription string `json:"ResponseDescription"`
    CustomerMessage     string `json:"CustomerMessage"`
    RequestID           string `json:"requestId"`
    ErrorCode           string `json:"errorCode"`
    ErrorMessage        string `json:"errorMessage"`
}
Enter fullscreen mode Exit fullscreen mode
  • STKPushRequestBody struct has the values required to build the request body. To learn more about the parameters,
    please check out this link.

  • STKPushRequestResponse struct is used to process the response after initiating a request.

To make the request, we will create a new method InitiateSTKPushRequest as follows

// InitiateSTKPushRequest makes a http request performing an STK push request
func (m *Mpesa) InitiateSTKPushRequest(body *STKPushRequestBody) (*STKPushRequestResponse, error) {
    url := fmt.Sprintf("%s/mpesa/stkpush/v1/processrequest", m.baseURL)

    requestBody, err := json.Marshal(body)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestBody))
    if err != nil {
        return nil, err
    }

    accessTokenResponse, err := m.generateAccessToken()
    if err != nil {
        return nil, err
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessTokenResponse.AccessToken))

    resp, err := m.makeRequest(req)
    if err != nil {
        return nil, err
    }

    stkPushResponse := new(STKPushRequestResponse)
    if err := json.Unmarshal(resp, &stkPushResponse); err != nil {
        return nil, err
    }

    return stkPushResponse, nil
}
Enter fullscreen mode Exit fullscreen mode
  • The above method handles the task of making the request to initiate the STK push request. After building the request, we call generateAccessToken method to attempt to generate an access token before making the request then set a new Authorization: Bearer header with our access token.
  • After making the request, we unmarshal our response body to create a new STKPushRequestResponse.

In order to test if we can make the request successfully and receive the STK push on our phone, we will update our main method as follows. The test credentials can be found here.

Select an app on the simulator, then scroll down and click on the icon on the bottom left to open a modal with the test credentials.

func main() {
    mpesa := NewMpesa(&MpesaOpts{
        ConsumerKey:    "your-consumer-key-goes-here",
        ConsumerSecret: "your-consumer-secret-goes-here",
        BaseURL:        "https://sandbox.safaricom.co.ke",
    })

    // The expected format is YYYYMMDDHHmmss
    timestamp := time.Now().Format("20060102150405")
    shortcode, passkey := "your-business-short-code-goes-here", "your-pass-key-goes-here"

    // base64 encoding of the shortcode + passkey + timestamp
    passwordToEncode := fmt.Sprintf("%s%s%s", shortcode, passkey, timestamp)
    password := base64.StdEncoding.EncodeToString([]byte(passwordToEncode))

    response, err := mpesa.InitiateSTKPushRequest(&STKPushRequestBody{
          BusinessShortCode: shortcode,
          Password:          password,
          Timestamp:         timestamp,
          TransactionType:   "CustomerPayBillOnline",
          Amount:            "10", // Amount to be charged when checking out
          PartyA:            "your-phone-number-goes-here", // 2547XXXXXXXX
          PartyB:            shortcode,
          PhoneNumber:       "your-phone-number-goes-here", // 2547XXXXXXXX
          CallBackURL:       "your-endpoint-to-receive-the-callback-on", // https://
          AccountReference:  "TEST",
          TransactionDesc:   "Payment via STK push.",
    })

      if err != nil {
        log.Fatalln(err)
      }

      fmt.Printf("%+v\n", response)
}
Enter fullscreen mode Exit fullscreen mode
  • We are getting the current timestamp then we concatenate the shortcode + passkey + timestamp to create the password used for encrypting the request sent which is base64 encoded string.
  • We then build our request body then make the request. If the request was successfully, you will receive an output similar to the one below.
  &{MerchantRequestID:32690-4496353-2 CheckoutRequestID:ws_CO_081020211030225587 ResponseCode:0 ResponseDescription:Success. Request accepted for processing CustomerMessage:Success. Request accepted for processing RequestID: ErrorCode: ErrorMessage:}
Enter fullscreen mode Exit fullscreen mode

If the request failed, you will receive the response similar to this.

  &{MerchantRequestID: CheckoutRequestID: ResponseCode: ResponseDescription: CustomerMessage: RequestID:1469-4474264-2 ErrorCode:400.002.02 ErrorMessage:Bad Request - Invalid BusinessShortCode}
Enter fullscreen mode Exit fullscreen mode

To get more insights on the error messages, please refer to the documentation.

Since the request is asynchronous, we pass the CallBackURL on the request body where we will receive the results of the transaction showing if it was processed successfully or not. Let's see how we can process the results on our callback URL.

We will use the default http server to process the payment response so let's create one. Add a new function called httpServer and update is as follows:

func httpServer() {
    stkPushCallbackHandler := func(w http.ResponseWriter, req *http.Request) {
      payload := new(STKPushCallbackResponse)

      if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
          log.Fatalln(err)
      }

      fmt.Printf("%+v\n", payload)
      fmt.Printf("Result Code: %d\n", payload.Body.StkCallback.ResultCode)
      fmt.Printf("Result Description: %s\n", payload.Body.StkCallback.ResultDesc)

      // TODO PROCESS THE RESPONSE
    }

    addr := ":8080"
    http.HandleFunc("/stk-push-callback", stkPushCallbackHandler)

    log.Printf("[*] Server started and running on port %s", addr)
    log.Fatal(http.ListenAndServe(addr, nil))
}
Enter fullscreen mode Exit fullscreen mode
  • The above function creates and starts a http server on port 8080.
  • We then add stkPushCallbackHandler which attempts to read and decode the request body. Any successful transaction will have a ResultCode of 0.

Remember to perform validation of the MpesaReceiptNumber against the CheckoutRequestID and MerchantRequestID which you can store after making the STK push request.

To test if we can make process the callback, update the main method to the code below then run the application.

func main() {
    httpServer()
}
Enter fullscreen mode Exit fullscreen mode

Open Postman and create a new POST request to http://localhost:8080/stk-push-callback. Click on the BODY tab then select the raw option then select JSON from the dropdown and paste the payload below then click on the SEND button then check your terminal output.

{
  "Body":{
    "stkCallback":{
      "ResultCode":0,
      "ResultDesc":"The service request is processed successfully.",
      "CallbackMetadata":{
        "Item":[
          {
            "Name":"Amount",
            "Value":1
          },
          {
            "Name":"MpesaReceiptNumber",
            "Value":"PJ14NE27PI"
          },
          {
            "Name":"Balance"
          },
          {
            "Name":"TransactionDate",
            "Value":20211009115859
          },
          {
            "Name":"PhoneNumber",
            "Value":254700000000
          }
        ]
      },
      "CheckoutRequestID":"ws_CO_01102021191752608272",
      "MerchantRequestID":"96131-41939755-1"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Thank you for following along. Next we will implement the B2C API.

Discussion (0)