Implementing M-pesa STK push and query in Ruby On Rails
Tags: ruby, rails, mpesa, webdev
Published: 2022-12-12 07:19:40

## Setup

### Daraja

- To get started head over to [daraja] (https://developer.safaricom.co.ke/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bownusi7tnbvq6le58tj.png)
- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8qxz51pllfszcriv1s8m.png)

![test credentials interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tuhykbqcxqiwuxw1ehb.png)
- Save your initiator password and passkey somewhere safe as you will need them later.

#### Ngrok
- Go to [ngrok](https://ngrok.com/) 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 <your authtoken> 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 ```.

```ruby
gem 'rack-cors'
gem 'rest-client'
```
- 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.
```ruby
config.hosts << /[a-z0-9]+\.ngrok\.io/
```
- 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.
```ruby
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :options]
  end
end
```
- 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.
```ruby
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"
```
** 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 <port number> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc18ulbwp9j16af55xr8.png)
![url from ngrok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i08zrkgvvfbenthhcaxo.png)
- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45ota39tqmt71wgs4vwc.png)

![rails index page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d7diwr94vuwzyc5ociax.png)
- If you get a Blocked Host error, check these [stackoverflow](https://stackoverflow.com/questions/53878453/upgraded-rails-to-6-getting-blocked-host-error) 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.
```ruby
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
```

- 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.
```ruby
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
```

##### Stk Push Request
- Under APIs -> M-pesa Express you can [simulate](https://developer.safaricom.co.ke/APIs/MpesaExpressSimulate) 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](https://developer.safaricom.co.ke/Documentation) -> Lipa Na M-pesa Online API -> Request Parameter Definition.
- Add the following code to app/controllers/mpesa_controller.rb.
```ruby
 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
```
- Navigate to routes.rb and add the following code.
```ruby
post 'stkpush', to: 'mpesas#stkpush'
```
- Open postman and make a post request to your ngrok-url/stkpush with the following parameters.
```json
{
    "phoneNumber": "2547xxxxxxxx",
    "amount": "1"
}
```
- The request sents an STK push to the phone number provided.
- Your response should look like this.
```json
{
    "MerchantRequestID": "xxxx-xxxx-xxxx-xxxx",
    "CheckoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX",
    "ResponseCode": "0",
    "ResponseDescription": "Success. Request accepted for processing",
    "CustomerMessage": "Success. Request accepted for processing"
}
```

![stkpush request postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwjmwcri138g5c5njxtz.png)
- 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](https://developer.safaricom.co.ke/APIs/MpesaExpressSimulate) 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.
```ruby
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
```
- Navigate to routes.rb and add the following code.
```ruby
post 'stkquery', to: 'mpesas#stkquery'
```
- Open postman and make a post request to your ngrok-url/stkquery with the following parameters.
```json
{
    "checkoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX"
}
```
- Your response should look like this.
```json
[
    "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"
    }
]
```

![stkquery postman ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uskmza6dph5csox21mo8.png)
- You can use the ResultCode to check if the payment was successful or not.
- Your mpesas_controller.rb should look like this.
```ruby
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
```
- Your routes.rb should look like this.
```ruby
Rails.application.routes.draw do
    post 'stkpush', to: 'mpesas#stkpush'
    post 'stkquery', to: 'mpesas#stkquery'
end
```
- I followed this [tutorial](https://medium.com/@sammaingi5/setup-mpesa-sdk-push-with-ruby-on-rails-d3ccaa035a58) and run into some errors and decided to write this article with some more clear steps.


- The full code for this [here](https://github.com/Annastacia-dev/daraja-test).

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!