sudo snap install ngrok
or download the zip file from the website and extract it.ngrok authtoken <your authtoken>
and replace rails new <app-name> --api
in my case rails new daraja-test --api
.bundle install
.gem 'rack-cors'
gem 'rest-client'
rails g resource Mpesa phoneNumber amount checkoutRequestID merchantRequestID mpesaReceiptNumber
.rails g model AccessToken token
.rails db:migrate
config.hosts << /[a-z0-9]+\.ngrok\.io/
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :options]
end
end
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_SHORT_CODE: '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 go to ngrok and run ngrok http <port number>
and replace
This will generate a url that you can use as your callback url.
In my case above ngrok http http://127.0.0.1:3000
and 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.
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
Wheeww!! That was a lot of configurations, let's now implement the code.
First require the rest-client gem require 'rest-client'
and add then 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
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 #{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
post 'stkpush', to: 'mpesa#stkpush'
{
"phoneNumber": "2547xxxxxxxx",
"amount": "1"
}
{
"MerchantRequestID": "xxxx-xxxx-xxxx-xxxx",
"CheckoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"CustomerMessage": "Success. Request accepted for processing"
}
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 #{ 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
post 'stkquery', to: 'mpesa#stkquery'
{
"checkoutRequestID": "ws_CO_XXXXXXXXXXXXXXXXXXXXXXXXX"
}
[
"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"
}
]
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 #{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 #{ 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 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
That's it for this tutorial. I hope you found it helpful. If you have any questions, feel free to reach out to me on email: annetotoh@gmail.com. THANK YOU!