DEV Community

Joram Wambugu
Joram Wambugu

Posted on

Part Two: Integrating MPesa B2C API

In the last tutorial, we learnt how to set up a developers, generate an access token and finally integrated lipa na mpesa API.
In this part, we will learn how we can integrate the B2C API. This API allows you to make MPesa transactions between a paybill (shortcode) and a customer's phone number.

This a continuation which assumes you have already set up your project as per the last tutorial.

The completed code can be found here.

What we will cover

Understanding the requirements

There are the requirements for production app. You will need to access MPesa Portal which is now accessible without the need of the MPesa certificate πŸŽ‰πŸŽ‰

  • Initiator username which is the API operator's username set on the portal.
  • Initiator Password is the password for the API operator.

To understand more about these requirements, please check the documentation under the B2C API.

Request setup

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


// B2CRequestBody is the body with the parameters to be used to initiate a B2C request
type B2CRequestBody struct {
    InitiatorName      string `json:"InitiatorName"`
    SecurityCredential string `json:"SecurityCredential"`
    CommandID          string `json:"CommandID"`
    Amount             string `json:"Amount"`
    PartyA             string `json:"PartyA"`
    PartyB             string `json:"PartyB"`
    Remarks            string `json:"Remarks"`
    QueueTimeOutURL    string `json:"QueueTimeOutURL"`
    ResultURL          string `json:"ResultURL"`
    Occassion          string `json:"Occassion"`
}

// B2CRequestResponse is the response sent back after initiating a B2C request.
type B2CRequestResponse struct {
    ConversationID           string `json:"ConversationID"`
    OriginatorConversationID string `json:"OriginatorConversationID"`
    ResponseCode             string `json:"ResponseCode"`
    ResponseDescription      string `json:"ResponseDescription"`
    RequestID                string `json:"requestId"`
    ErrorCode                string `json:"errorCode"`
    ErrorMessage             string `json:"errorMessage"`
}
Enter fullscreen mode Exit fullscreen mode
  • B2CRequestBody struct has the values required to build the request body. To learn more about the parameters, please check out this link.

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

Generating security credentials

The B2C API requires us to provide SecurityCredential in the request body which can be generated by encrypting the API initiator password using the public key certificate provided by Safaricom.

Please note that at the time of writing, the provided links to download the certificates were broken. If you encounter the same issue, please download them here.

There are two sets of certificates, the sandbox and production used when your application has been approved to go live.
Go ahead and download sandbox certificate, then create certificates directory and move the downloaded certificate there.

To generate the SecurityCredential, we will create a helper function GenerateSecurityCredentials as follows:

// getSecurityCredentials returns the encrypted password using the public key of the specified environment
func GenerateSecurityCredentials(password string, isOnProduction bool) (string, error) {
    path := "./certificates/production.cer"

    if !isOnProduction {
        path = "./certificates/sandbox.cer"
    }

    f, err := os.Open(path)
    if err != nil {
        return "", err
    }

    defer func(f *os.File) {
        _ = f.Close()
    }(f)

    contents, err := io.ReadAll(f)
    if err != nil {
        return "", err
    }

    block, _ := pem.Decode(contents)

    var cert *x509.Certificate

    cert, err = x509.ParseCertificate(block.Bytes)
    if err != nil {
        return "", err
    }

    rsaPublicKey := cert.PublicKey.(*rsa.PublicKey)
    reader := rand.Reader

    encryptedPayload, err := rsa.EncryptPKCS1v15(reader, rsaPublicKey, []byte(password))
    if err != nil {
        return "", err
    }

    securityCredentials := base64.StdEncoding.EncodeToString(encryptedPayload)
    return securityCredentials, nil
}
Enter fullscreen mode Exit fullscreen mode
  • The function takes in a password which is the API initiator password and isOnProduction which is used to determine the certificate to use.
  • We attempt to read and parse the contents certificate to return an encrypted password.

We can test if the code works by updating our main method to the snippet below.

func main() {
    securityCredentials, err := GenerateSecurityCredentials("test-password", false)
    if err != nil {
        log.Fatalln(err)
    }

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

If we run our code, an output similar to the one below will be printed.

NCV4uID/dA5OpNmgtwk2UJj1NXyEa0KNyhxm/8fb82Cojo6UQKaw1Lk2MHzD+kX3NXQppwaoTFjkPGNjlWWBO2VDu15IGZGWSOWdS6Wuqc8U8BqVWXcTLCMA5UujQBEfSJsIYROGdbeqUSLE88I/3NNH1nINDgI5A91LX/Fwouewy4S5aogcb9M+KrcVDcoNSkmCY+S4UFlUGMUPAIMxIzZh6z39LCXhM/ARItbJJXK6Nj31cmIimXE3UWTLq8uqHNwafDAidjgUenwTOeO2/pJ/x3rjbM3RyOHd2JTlM3eazBqSSIFh2YaVxrKtCvOtowMXPDQ//1ZKzErnFZtViw==
Enter fullscreen mode Exit fullscreen mode

Making the B2C API request

For us to make the request, we will create two new methods setupHttpRequestWithAuth and InitiateB2CRequest.

// setupHttpRequestWithAuth is a helper method aimed to create a http request adding
// the Authorization Bearer header with the access token for the Mpesa app.
func (m *Mpesa) setupHttpRequestWithAuth(method, url string, body []byte) (*http.Request, error) {
    req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
    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))

    return req, nil
}

// InitiateB2CRequest makes a http request performing a B2C payment request.
func (m *Mpesa) InitiateB2CRequest(body *B2CRequestBody) (*B2CRequestResponse, error) {
    url := fmt.Sprintf("%s/mpesa/b2c/v1/paymentrequest", m.baseURL)

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

    req, err := m.setupHttpRequestWithAuth(http.MethodPost, url, requestBody)
    if err != nil {
        return nil, err
    }

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

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

    return b2cResponse, nil
}
Enter fullscreen mode Exit fullscreen mode
  • setupHttpRequestWithAuth set ups and returns a http request with the authorization set up.
  • InitiateB2CRequest makes a http request with the generated access token. After a successful request is made, we unmarshal the response body to create a new B2CRequestResponse

In order to test if we can make the request successfully, 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",
    })

    securityCredentials, err := GenerateSecurityCredentials("your-initiator-password", false)
    if err != nil {
        log.Fatalln(err)
    }

    response, err := mpesa.InitiateB2CRequest(&B2CRequestBody{
        InitiatorName:      "your-initiator-name-goes-here",
        SecurityCredential: securityCredentials,
        CommandID:          "BusinessPayment",
        Amount:             "10",
        PartyA:             "your-business-short-code-goes-here",
        PartyB:             "your-phone-number-goes-here",
        Remarks:            "your-remarks-go-here",
        QueueTimeOutURL:    "your-endpoint-to-receive-notifications-in-case-request-times-out",
        ResultURL:          "your-endpoint-to-receive-the-notifications",
        Occassion:          "your-occasion-goes-here",
    })

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

    fmt.Printf("%+v\n", response)
}
Enter fullscreen mode Exit fullscreen mode
  • We are using the GenerateSecurityCredentials to get the security credentials using the InitiatorPassword from the test credentials. The second parameter isOnProduction is then set to false since we are on sandbox.
  • 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.
  &{ConversationID:AG_20211014_00007410b1727ef09241 OriginatorConversationID:46629-16123460-2 ResponseCode:0 ResponseDescription:Accept the service request successfully. RequestID: ErrorCode: ErrorMessage:}
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 ResultURL webhook for receiving notifications upon processing of the payment request. The QueueTimeOutURL is a webhook used when the payment request has timed out while awaiting processing.

To test if we can process the callback, we will create a new handler on the httpServer function and add a new struct B2CCallbackResponse as follows:

// B2CCallbackResponse has the results of the callback data sent once we successfully make a B2C request.
type B2CCallbackResponse struct {
    Result struct {
        ResultType               int    `json:"ResultType"`
        ResultCode               int    `json:"ResultCode"`
        ResultDesc               string `json:"ResultDesc"`
        OriginatorConversationID string `json:"OriginatorConversationID"`
        ConversationID           string `json:"ConversationID"`
        TransactionID            string `json:"TransactionID"`
        ResultParameters         struct {
            ResultParameter []struct {
                Key   string      `json:"Key"`
                Value interface{} `json:"Value"`
            } `json:"ResultParameter"`
        } `json:"ResultParameters"`
        ReferenceData struct {
            ReferenceItem struct {
                Key   string `json:"Key"`
                Value string `json:"Value"`
            } `json:"ReferenceItem"`
        } `json:"ReferenceData"`
    } `json:"Result"`
}
Enter fullscreen mode Exit fullscreen mode
func httpServer() {
    b2cRequestCallbackHandler := func(w http.ResponseWriter, req *http.Request) {
        payload := new(B2CCallbackResponse)

        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.Result.ResultCode)
        fmt.Printf("Result Description: %s\n", payload.Result.ResultDesc)
    }

    addr := ":8080"
    http.HandleFunc("/b2c-callback", b2cRequestCallbackHandler)

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

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/b2c-callback.
  • Click on the BODY tab then select the raw option then select JSON from the dropdown and paste the payload below.
  • Click on the SEND button then check your terminal output for the printed response body.
{
   "Result":{
      "ResultType":0,
      "ResultCode":0,
      "ResultDesc":"The service request is processed successfully.",
      "OriginatorConversationID":"46629-16123460-2",
      "ConversationID":"AG_20211014_00007410b1727ef09241",
      "TransactionID":"PJ14NE27PI",
      "ResultParameters":{
         "ResultParameter":[
            {
               "Key":"TransactionAmount",
               "Value":10
            },
            {
               "Key":"TransactionReceipt",
               "Value":"PJ14NE27PI"
            },
            {
               "Key":"B2CRecipientIsRegisteredCustomer",
               "Value":"Y"
            },
            {
               "Key":"B2CChargesPaidAccountAvailableFunds",
               "Value":120.00
            },
            {
               "Key":"ReceiverPartyPublicName",
               "Value":"254700000000 - Go Gopher"
            },
            {
               "Key":"TransactionCompletedDateTime",
               "Value":"14.10.2021 10:26:50"
            },
            {
               "Key":"B2CUtilityAccountAvailableFunds",
               "Value":120.00
            },
            {
               "Key":"B2CWorkingAccountAvailableFunds",
               "Value":120.00
            }
         ]
      },
      "ReferenceData":{
         "ReferenceItem":{
            "Key":"QueueTimeoutURL",
            "Value":"https:\/\/internalsandbox.safaricom.co.ke\/mpesa\/b2cresults\/v1\/submit"
         }
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

That's all for this part. Thank you for following along.

Top comments (0)