DEV Community

Cover image for Implementing M-pesa STK push and query in Ruby On Rails
Annastacia Kioko
Annastacia Kioko

Posted on • Edited on

Implementing M-pesa STK push and query in Ruby On Rails

Setup

Daraja

  • To get started head over to daraja and sign up for a developer account or login if you already have one.
  • On my apps tab, create a new sandbox app and name it whatever you want, then tick all the check boxes and click on create app.
  • You will be redirected to the app details page where you will find your consumer key and consumer secret.

consumerkey and secret

  • Save these somewhere safe as you will need them later.
  • Navigate to the APIs tab and on M-pesa Express click on Simulate, on the input prompt select the app you just created.
  • This will auto populate some fields for you, you can leave them as they are.
  • Scroll down and click on test credentials.

test credentials icon

test credentials interface

  • Save your initiator password and passkey somewhere safe as you will need them later.

Ngrok

  • Go to ngrok and sign up for a free account or login if you already have one.
  • To install on ubuntu sudo snap install ngrok or download the zip file from the website and extract it.
  • To connect your account to ngrok run ngrok authtoken <your authtoken> and replace with your authtoken.
  • We will get back to ngrok later, let's first setup our rails app.

Rails

  • Create a new rails app rails new <app-name> --api in my case rails new daraja-test --api .
  • Add the following gems to your gemfile and run bundle install .
gem 'rack-cors'
gem 'rest-client'
Enter fullscreen mode Exit fullscreen mode
  • We need to create an M-pesa resource, all datatypes are strings.
  • Run rails g resource Mpesa phoneNumber amount checkoutRequestID merchantRequestID mpesaReceiptNumber .
  • We also need a model for the access token, run rails g model AccessToken token .
  • Run rails db:migrate

Configurations

  • Navigate to config/environments/development.rb and add the following code.
config.hosts << /[a-z0-9]+\.ngrok\.io/
Enter fullscreen mode Exit fullscreen mode
  • This will allow us to access our rails app from ngrok.
  • Navigate to config/initializers/cors.rb and add the following code or uncomment the existing code.
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :options]
  end
end
Enter fullscreen mode Exit fullscreen mode
  • Be sure to replace origins 'example.com' with origins '*'if you uncomment existing code instead of adding the above code.

Environment variables

  • Inside the config folder create a file called local_env.yml and add the following code.
MPESA_CONSUMER_KEY: '<your consumer key>'
MPESA_CONSUMER_SECRET: '<your consumer secret>'
MPESA_PASSKEY: '<your passkey>'
MPESA_SHORTCODE: '174379'
MPESA_INITIATOR_NAME: 'testapi'
MPESA_INITIATOR_PASSWORD: '<your initiator password>'
CALLBACK_URL: '< your ngrok url>'
REGISTER_URL: "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl"
Enter fullscreen mode Exit fullscreen mode

** Note about the CALLBACK_URL **

  • To get you callback url first run your rails server rails s and copy the url from the terminal.
  • Then navigate to a new terminal and run ngrok http <port number> and replace with the port number from your rails server.
  • This will generate a url that you can use as your callback url.

url from running rails s
url from ngrok

  • In my case above ngrok http://127.0.0.1:3000 or ngrok http 3000 the url generated was https://5d5b-105-161-115-83.in.ngrok.io
  • Note that the url generated by ngrok changes every time you run it, so you will need to update your local_env.yml file with the new url every time you run ngrok.
  • Navigate to the ngrok url, you should see the page below, click on visit site which should take you to your rails app. ngrok interface

rails index page

  • If you get a Blocked Host error, check these stackoverflow solutions.
  • In my case I had to replace config.hosts << /[a-z0-9]+\.ngrok\.io/ with config.hosts.clear in config/environments/development.rb. This however is not recommended for production. - Remember to add your local_env.yml file to your .gitignore file.
  • We need rails to load our environment variables, to do this add the following code to config/application.rb.
config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end
Enter fullscreen mode Exit fullscreen mode
  • Wheeww!! That was a lot of configurations, let's now implement the code.

Implementing the code

  • First we need to write private methods to generate and get an access token from the Authorization API.
  • Generate Access Token Request -> Gives you a time bound access token to call allowed APIs in the sandbox.
  • 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.
  • Add the following code to app/controllers/mpesa_controller.rb.
  • First require the rest-client gem require 'rest-client' then add the following code.
private

    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}"
        })
        res
    end

    def get_access_token
        res = generate_access_token_request()
        if res.code != 200
        r = generate_access_token_request()
        if res.code != 200
        raise MpesaError('Unable to generate access token')
        end
        end
        body = JSON.parse(res, { symbolize_names: true })
        token = body[:access_token]
        AccessToken.destroy_all()
        AccessToken.create!(token: token)
        token
    end
Enter fullscreen mode Exit fullscreen mode
Stk Push Request
  • Under APIs -> M-pesa Express you can simulate a stk push request by selecting your app and changing Party A and Phone Number to your phone number.
  • Looking at the JSON the request body has the following parameters; { BusinessShortCode - The organization shortcode used to receive the transaction. Password - The password for encrypting the request.(Base64 encoded string,a combination of your BusinessShortCode, Passkey and Timestamp) Timestamp - The timestamp of the transaction in the format yyyymmddhhiiss TransactionType - The type of transaction (CustomerPayBillOnline or CustomerBuyGoodsOnline) Amount - The amount being transacted PartyA - The phone number sending the money. PartyB - The organization shortcode receiving the funds.Can be the same as the business shortcode. PhoneNumber - The mobile number to receive the STK push.Can be the same as Party A. CallBackURL - The url to where responses from M-Pesa will be sent to. Should be valid and secure. AccountReference - Value displayed to the customer in the STK Pin prompt message. TransactionDesc - A description of the transaction. }
  • You can read more on their documentation -> Lipa Na M-pesa Online API -> Request Parameter Definition.
  • Add the following code to app/controllers/mpesa_controller.rb.
 def stkpush
        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': 'Codearn',
        'TransactionDesc': "Payment for Codearn premium"
        }.to_json

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

        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: response
    end
Enter fullscreen mode Exit fullscreen mode
  • Navigate to routes.rb and add the following code.
post 'stkpush', to: 'mpesas#stkpush'
Enter fullscreen mode Exit fullscreen mode
  • Open postman and make a post request to your ngrok-url/stkpush with the following parameters.
{
    "phoneNumber": "2547xxxxxxxx",
    "amount": "1"
}
Enter fullscreen mode Exit fullscreen mode
  • The request sents an STK push to the phone number provided.
  • Your response should look like this.
{
    "MerchantRequestID": "xxxx-xxxx-xxxx-xxxx",
    "CheckoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX",
    "ResponseCode": "0",
    "ResponseDescription": "Success. Request accepted for processing",
    "CustomerMessage": "Success. Request accepted for processing"
}
Enter fullscreen mode Exit fullscreen mode

stkpush request postman

  • Save the CheckoutRequestID for the next step.

Stk Query Request

  • We can use the mpesa query to check if the payment was successful or not.
  • Under APIs -> M-pesa Express you can simulate a query a stk push request by selecting your app and inputing the CheckoutRequestID you got from the previous step.
  • The request body has the following parameters; { BusinessShortCode - The organization shortcode used to receive the transaction. Password - The password for encrypting the request.(Base64 encoded string,a combination of your BusinessShortCode, Passkey and Timestamp) Timestamp - The timestamp of the transaction in the format yyyymmddhhiiss CheckoutRequestID - The CheckoutRequestID used to identify the transaction on M-Pesa. }
  • Add the following code to app/controllers/mpesa_controller.rb.
def stkquery
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query"
        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,
        'CheckoutRequestID': params[:checkoutRequestID]
        }.to_json

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

        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: response
    end
Enter fullscreen mode Exit fullscreen mode
  • Navigate to routes.rb and add the following code.
post 'stkquery', to: 'mpesas#stkquery'
Enter fullscreen mode Exit fullscreen mode
  • Open postman and make a post request to your ngrok-url/stkquery with the following parameters.
{
    "checkoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX"
}
Enter fullscreen mode Exit fullscreen mode
  • Your response should look like this.
[
    "success",
    {
        "ResponseCode": "0",
        "ResponseDescription": "The service request has been accepted successsfully",
        "MerchantRequestID": "8491-75014543-2",
        "CheckoutRequestID": "ws_CO_12122022094855872768372439",
        "ResultCode": "1032",
        "ResultDesc": "Request cancelled by user"
    }
]
Enter fullscreen mode Exit fullscreen mode

stkquery postman

  • You can use the ResultCode to check if the payment was successful or not.
  • Your mpesas_controller.rb should look like this.
class MpesasController < ApplicationController

    require 'rest-client'

    # stkpush
     def stkpush
        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': 'Codearn',
        'TransactionDesc': "Payment for Codearn premium"
        }.to_json

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

        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: response
    end

    # stkquery

    def stkquery
        url = "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query"
        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,
        'CheckoutRequestID': params[:checkoutRequestID]
        }.to_json

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

        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: response
    end

    private

    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}"
        })
        res
    end

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


end
Enter fullscreen mode Exit fullscreen mode
  • Your routes.rb should look like this.
Rails.application.routes.draw do
    post 'stkpush', to: 'mpesas#stkpush'
    post 'stkquery', to: 'mpesas#stkquery'
end
Enter fullscreen mode Exit fullscreen mode
  • I followed this tutorial and run into some errors and decided to write this article with some more clear steps.

  • The full code for this here.

I hope you found this helpful. If you have any questions, feel free to reach out to me on email: annetotoh@gmail.com.
THANK YOU!

Top comments (4)

Collapse
 
lucasmasaba profile image
Khusiima Luke Masaba

Thanks for sharing this article. For those getting the block host error, for the free tier of ngrok, you should use this in config/environments/development.rb:

config.hosts << /[a-z0-9\-]+\.ngrok-free\.app/
Enter fullscreen mode Exit fullscreen mode
Collapse
 
collins77 profile image
Collins7-7

This was really helpful! Thank you.

Collapse
 
gmatieso_95 profile image
Gmatieso

Nice Article

Collapse
 
kidd254 profile image
Lawrence Kioko

Hello, nice article... however i have some question... if i deploy my api will i still use the ngrok url or will i use the api url