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
- Request setup
- Generating security credentials
- Making the B2C API request
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"`
}
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
}
- The function takes in a
password
which is the API initiator password andisOnProduction
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)
}
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==
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
}
-
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 newB2CRequestResponse
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)
}
- We are using the
GenerateSecurityCredentials
to get the security credentials using the InitiatorPassword from the test credentials. The second parameterisOnProduction
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:}
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"`
}
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))
}
-
httpServer
creates and starts a http server on port8080
. -
b2cRequestCallbackHandler
attempts to read and decode the request body. Any successful transaction will have aResultCode
of0
.
To test if we can make process the callback, update the main method to the code below then run the application.
func main() {
httpServer()
}
- Open Postman and create a new
POST
request tohttp://localhost:8080/b2c-callback
. - Click on the
BODY
tab then select theraw
option then selectJSON
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"
}
}
}
}
That's all for this part. Thank you for following along.
Top comments (0)