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

anne46

Annastacia Kioko

Posted on December 12, 2022

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!

💖 💪 🙅 🚩
anne46
Annastacia Kioko

Posted on December 12, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related