Setup Mpesa SDK Push with Ruby On Rails

Maingi Samuel
6 min readOct 30, 2022

--

After many hours of researching for a gem or article explaining the process of intergrating M-Pesa SDK Push to ruby on rails but kept on hitting multiple dead ends. I decided to do create a Rails mini app api to demo the basics of a simple SDK Push and polling (checking if payment is successfull or might have encounted any errors)

Can Access the Code here -> https://github.com/layersony/ruby-on-rails-mpesa-payment-blog

Requirements

  1. M-Pesa Daraja Account and a sandbox app Create one here
  2. Ruby and Ruby on Rails
  3. Ngrok download here
  4. Ruby Gems -> rest-client 2.0 and rack-cors

Create M-Pesa Sandbox App

Navigate to https://developer.safaricom.co.ke/MyApps

Click on create New app

Make sure you give your sandbox app a name then tick all the checkboxes

After your App has being created, Click on the show Key/Secret to get the Consumer Key and Consumer Secret

Create Ruby on Rails M-Pesa Mini Api

Create a mininal Rails app

rails new ROR_Mpesa — api — minimal

Create a payment resource

rails g resource Mpesa checkoutRequestID merchantRequestID amount mpesaReceiptNumber phoneNumber — no-test-framework

then,

rails db:migrate db:seed

Open your favorite Code Editior and lets start coding

Basic Configuration

Lets start with basic configs

Navigate to config/environments/development.rb and add the following code. this will enable you have dynamic urls

config.hosts << /[a-z0–9-.]+\.ngrok\.io/

then navigate to config/initializrs/cors.rb and replace with this code

Config environment variables

Inside config folder create a file called local_env.yml and set your secrets in there. DO NOT FORGET to add the local_env.yml to your .gitignore

to access the secrets use ENV[“MPESA_SHORTCODE”]

But we need rails to know where our secrets are so add the following code to config/application.rb inside class Application < Rails::Application

config.before_configuration doenv_file = File.join(Rails.root, 'config', 'local_env.yml')YAML.load(File.open(env_file)).each do |key, value|ENV[key.to_s] = valueend if File.exists?(env_file)end

Full module should look like this

The Main Code

Navigate to mpesas_controller.rb and lets create the following functions get_access_token, generate_access_token_request, stkpush, callback_url

Lets start with Private functions

Generate Access Token Request -> Gives you a time bound access token to call allowed APIs it provides you with an access token thou it returns the response form api

Get Access Token -> Used to check if generate_acces_token_request is successful or not then it reads the responses and extracts the access token from the response and saves it to the database

Create a rails model for access_token

rails g model AccessToken token — no-test-framework

First import rest-client inside MpesasContoller in the first line

require ‘rest-client’

Private functions

generate_access_token_request

def generate_access_token_request@url = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"@consumer_key =  ENV['MPESA_CONSUMER_KEY']@consumer_secret = ENV['MPESA_CONSUMER_SECRET']@userpass = Base64::strict_encode64("#{@consumer_key}:#{@consumer_secret}")@headers = {Authorization: "Bearer #{@userpass}"}res = RestClient::Request.execute(url: @url, method: :get, headers:{Authorization: "Basic #{@userpass}"})resend

get_access_token

def get_access_tokenres = generate_access_token_request()if res.code != 200r = generate_access_token_request()if res.code != 200raise MpesaError('Unable to generate access token')endendbody = JSON.parse(res, { symbolize_names: true })token = body[:access_token]AccessToken.destroy_all()AccessToken.create!(token: token)tokenend

Let’s Create the STKPush function

Do use the mpesa daraja express located in API’s for more information

stkpush -> will break it down in various parts

  1. Payload

Phonenumber and amount should be provided for this is will be Post request

timestamp -> should be in this format “%Y%m%d%H%M%S”

password -> should be encoded with base64 format (business_short_code + mpesa_passkey+timestamp)

phoneNumber = params[:phoneNumber]amount = params[:amount]url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"business_short_code = ENV["MPESA_SHORTCODE"]password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")payload = {'BusinessShortCode': business_short_code,'Password': password,'Timestamp': timestamp,'TransactionType': "CustomerPayBillOnline",'Amount': amount,'PartyA': phoneNumber,'PartyB': business_short_code,'PhoneNumber': phoneNumber,'CallBackURL': "#{ENV["CALLBACK_URL"]}/callback_url",'AccountReference': 'Trial ROR Mpesa','TransactionDesc': "ROR trial"}.to_json

2. Headers

headers = {content_type: 'application/json',Authorization:"Bearer #{get_access_token}"}

3. Send Response

response = RestClient::Request.new({method: :post,url: url,payload: payload,headers: headers}).execute do |response, request|case response.codewhen 500[ :error, JSON.parse(response.to_str) ]when 400[ :error, JSON.parse(response.to_str) ]when 200[ :success, JSON.parse(response.to_str) ]elsefail "Invalid response #{response.to_str} received."endendrender json: response

Full function should look this

Create Route for the functions

navigate to config/routes

Fire up you ngrok using this command

ngrok http http://127.0.0.1:3000

Call the stkpush url using the https://…… link

https://df77-102-2-135-121.ngrok.io/stkpush

Open up Insomia or Postman, create a new request and the request method should be Post.

the data should be in Json format

{
“phoneNumber”:”2547XXXXXXXXX”,
“amount”:”1"
}

When request is send an SDK Push Prompt is sent to phoneNumber provided above

The response after the request is

[
"success",
{
"MerchantRequestID": "18552-53113685-1",
"CheckoutRequestID": "ws_CO_30102022195943475748681351",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"CustomerMessage": "Success. Request accepted for processing"
}
]

Confirm Payment Using Mpesa

After one has paid you can use the mpesa query api to check if the payment was successful or not

Navigate to mpesas_controller and add the following code

def polling_payment  # Check if payment has been paid  checkoutId = params[:checkoutId]  timestamp = "#{Time.now.strftime "%Y%m%d%H%M%S"}"  business_short_code = ENV["MPESA_SHORTCODE"]  password = Base64.strict_encode64("#{business_short_code}#{ENV["MPESA_PASSKEY"]}#{timestamp}")  url = ENV['QUERY_URL']  headers = {    content_type: 'application/json',    Authorization: "Bearer #{get_access_token}"  }  payload = {    'BusinessShortCode' => business_short_code,    'Password' => password,    'Timestamp' => timestamp,    'CheckoutRequestID' => checkoutId  }.to_json
response = RestClient::Request.new({ method: :post, url: url, payload: payload, headers: headers }).execute do |response, request| case response.code when 500 [ :error, JSON.parse(response.to_str) ] when 400 [ :error, JSON.parse(response.to_str) ] when 200 [ :success, JSON.parse(response.to_str) ] else fail "Invalid response #{response.to_str} received." end end render json: { msg: response }end

Then create a route for payment polling

Switch to Postman or Insomnia and create a Post request

https://df77-102-2-135-121.ngrok.io/polling_payment

required data for this endpoint is CheckoutRequestID (retrive this from the stkpush response)

"CheckoutRequestID": "ws_CO_30102022195943475748681351"

Json format should be

{
"checkoutRequestID":"ws_CO_30102022195943475748681351"
}

The Response should be

[
"success",
{
"ResponseCode": "0",
"ResponseDescription": "The service request has been accepted successsfully",
"MerchantRequestID": "18552-53113685-1",
"CheckoutRequestID": "ws_CO_30102022195943475748681351",
"ResultCode": "0",
"ResultDesc": "The service request is processed successfully."
}
]

You can use ResultDesc as a message prompt for your Client

NOTES:

You can access the Code Here

Remember to hide you Daraja Consumer Key and Secret

Do Reach Out for more Information, Question or Feedback concerning the above topic

--

--