USING CHPTER API FOR MPESA PAYMENTS IN ELIXIR AND PHOENIX LIVE VIEW

Michael Munavu
9 min readOct 22, 2023

Payments are something we all need in our systems, In Kenya, the most popular payment API but their documentation is hell in my opinion and that is why I have opted for chpter in my systems, I have implemented it with Elixir and Phoenix Live View to create ticketing systems that never fail.

All these payment systems work the same, It took some time for me to understand the whole concept, but I’ll try and make this journey shorter for you.

Anytime you get a prompt to pay for something via mpesa in a web system , what happens is that after you pay , whether successful or not , a POST request with a body containing some data is sent to a certain endpoint . You may have heard of it being called a callback url. Yes , a callback url , this is where MPESA or chpter in this case will send a POST request with details showing whether the payment was successful or not .

In this article , we will build a simple system with a orders , for you to make the order , you will have to complete payment . We will be using an API for this provided by chpter , https://v3.chpter.co , Here is their documentation https://chpter.gitbook.io/chpter-3.0-public-api-reference/ .

As I wrote this article, I also decided to make a hex package to make it easy for us all, Here it is https://hexdocs.pm/chpter/0.1.1/Chpter.html and the documentation is here https://hexdocs.pm/chpter/Chpter.html , I’d advise you to read the whole article as it will help you understand the whole process flow better .

You can also find the GitHub repository for this project here .

https://github.com/MICHAELMUNAVU83/chpter_phoenix

Enough with the intro, let us get our hands dirty.

We create a new Phoenix app with the following command .

mix phx.new chpter_phoenix

Configure your database by setting the username and password in

config/dev.exs

Configure your database with

mix ecto.setup

Now let us working on creating some models , here no one signs up , the flow is such that , users come into the system and can make a new order , for this , they have to make a payment

Our model will look as such

Before we start working on these , Let us first work on the callback url , Looking at the documentation chpter provides , https://chpter.gitbook.io/chpter-3.0-public-api-reference/ ,they say the callack url needs to have the following columns

Message: string , Success: boolean , Status: integer , Amount: string , transaction_code:string and transaction_reference:string .

We need to create an endpoint that takes such a body , I realized in elixir, it is not possible to access columns starting with capital letters , in that case , for each of the columns that start with a capital letter , in our db , they will start with a small letter .

Thus for our callback url , our model will have

message: string , success: boolean , status: integer , amount: string , transaction_code:string and transaction_reference:string .

Every time chpter sends a POST request , we will intercept it before it enters our db and create a new map with our column names , you will see later how this works .

First , we create our model for the callback url , we will use the

mix phx.gen.json

command

on your terminal run

mix phx.gen.json Transactions Transaction transactions message success:boolean status:integer amount transaction_code transaction_reference

Add the resource to your :api scope in lib/chpter_phoenix_web/router.ex:

resources "/transactions", TransactionController, except: [:new, :edit]

Such that you have this in your router file

 scope "/api", ChpterPhoenixWeb do
pipe_through :api
resources "/transactions", TransactionController, except: [:new, :edit]
end

Remember to update your repository by running migrations:

mix ecto.migrate

Now if we go to fire up our server with

mix phx.server

and go to localhost:4000/api/transactions , we see this

Currently , we have no data in our api endpoint .

Let us go to our transaction schema file in chpter_phoenix/transactions/transaction.ex and change

def changeset(transaction, attrs) do
transaction
|> cast(attrs, [:message, :success, :status, :amount, :transaction_code, :transaction_reference])
|> validate_required([:message, :success, :status, :amount, :transaction_code, :transaction_reference])
end

To

def changeset(transaction, attrs) do
transaction
|> cast(attrs, [:message, :success, :status, :amount, :transaction_code, :transaction_reference])
|> validate_required([:success, :status, :transaction_reference])
end

The body sent by chpter may have nil values for the message and amount but will always send data in regards to the transaction reference , status and success .

Now we move to our transaction controller , Remember , chpter sends the body in the format

%{
"Message" => "message",
"Amount" => "1.000",
"Status" => 200,
"Success" => true,
"transaction_code"=> "RTJVSJF69",
"transaction_reference" => "254740769596"
}

But in our database , we have the columns in starting in capital letters starting in small letters .

The whole idea here is to take the params we get when a body is posted and make a new map that can get into our database with the corresponding columns .

Let us to go the controller file in chpter_phoenix_web/controllers/transactions/transaction.ex

Edit the create function to

 def create(conn, transaction_params) do
new_transaction_params = %{
"message" => transaction_params["Message"],
"success" => transaction_params["Success"],
"status" => transaction_params["Status"],
"amount" => transaction_params["Amount"],
"transaction_code" => transaction_params["transaction_code"],
"transaction_reference" => transaction_params["transaction_reference"]
}



with {:ok, %Transaction{} = transaction} <-
Transactions.create_transaction(new_transaction_params) do
conn
|> put_status(:created)
|> put_resp_header("location", Routes.transaction_path(conn, :show, transaction))
|> render("show.json", transaction: transaction)
end
end

Now we are set on the callback URL side, if you have Postman, we can test this out by sending a body to /api/transactions .

This means that we are good to go , Now we can work on creating the orders .

In your terminal run

mix phx.gen.live Orders Order orders phone_number customer_name customer_email  delivery_location

Add the live routes to your browser scope in lib/chpter_phoenix_web/router.ex:

live "/orders", OrderLive.Index, :index
live "/orders/new", OrderLive.Index, :new
live "/orders/:id/edit", OrderLive.Index, :edit
live "/orders/:id", OrderLive.Show, :show
live "/orders/:id/show/edit", OrderLive.Show, :edit

Remember to update your repository by running migrations:

mix ecto.migrate

Now if we fire up our server again ,

mix phx.server 

And we go to /orders , we see

And we can also create a new order

First and foremost , we would like to validate this data , ensure Numbers start with 254 and are 12 digits , emails have a certain format , for this , we can add some code to our orders schema files .

Head over to lib/chpter_phoenix/orders/order.ex and edit the changeset function to

 def changeset(order, attrs) do
order
|> cast(attrs, [:phone_number, :customer_name, :customer_email, :delivery_location])
|> validate_required([:phone_number, :customer_name, :customer_email, :delivery_location])
|> validate_format(
:phone_number,
~r/^254\d{9}$/,
message: "Number has to start with 254 and have 12 digits"
)
|> validate_format(:customer_email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
end

Here , we have added 2 extra validations using regex to ensure the right format of data enters our db .

Now if we give it a try

This now works well , Let us now dive into payment .

For most payment systems , it is divided into 2 steps

  1. Initiation
  2. Checking for payment .

Initiation

Here , we send a post request to https://api.chpter.co/v1/initiate/mpesa-payment with the following body format as shown in their documentation.

   body = %{
customer_details: %{
"full_name" => "name,
"location" => "location",
"phone_number" => phone_number,
"email" => email
},
products: [

],
amount: %{
"currency" => "KES",
"delivery_fee" => 0.0,
"discount_fee" => 0.0,
"total" => 1
},
callback_details: %{
"transaction_reference" => phone_number,
"callback_url" => callback url
}
}

And the header tag as follows .


headers = [
{
"Content-Type",
"application/json"
},
{
"Api-Key",
"api-key"
}
]

In the header tag , we are sending 2 things , the Content Type and Api Key , the api key is provided by chpter upon registration and provision of KYC docs .

Now onto the body , for me , the most important details are

the customer details , the total amount , the callback url and the transaction reference .

I personally like having the transaction reference as a random unique number , there are various ways to do this , I use Timex for this . Using timex , we can get umique numbers as such 20231021160626 that we can concat with a phone number to make it really unique .

We will be using HTTPoison and Poison to make http requests so we need to add them to our mix.exs in addition to the

 {:poison, "~> 5.0"},
{:httpoison, "~> 2.1"},
{:timex, "~> 3.0"},

Run

mix deps.get

Since elixir is functional we can break this down into functions , one for getting the header , another one for the body and one for initiating payment.

In lib/chpter_phoenix , create a file and call it chpter.ex , this will be a module .

defmodule ChpterPhoenix.Chpter do



end

For initiation , we can have these functions

defmodule ChpterPhoenix.Chpter do
def initiate_payment(
api_key,
phone_number,
name,
email,
amount,
location,
callback_url,
transaction_reference
) do
header = header(api_key)

body =
body(
phone_number,
email,
name,
location,
amount,
callback_url,
transaction_reference
)

url = "https://api.chpter.co/v1/initiate/mpesa-payment"

request_body = Poison.encode!(body)

HTTPoison.post(url, request_body, header)
end

defp header(api_key) do
[
{
"Content-Type",
"application/json"
},
{
"Api-Key",
api_key
}
]
end

defp body(
phone_number,
email,
name,
location,
amount,
callback_url,
transaction_reference
) do
%{
customer_details: %{
"full_name" => name,
"location" => location,
"phone_number" => phone_number,
"email" => email
},
products: [],
amount: %{
"currency" => "KES",
"delivery_fee" => 0.0,
"discount_fee" => 0.0,
"total" => amount
},
callback_details: %{
"transaction_reference" => transaction_reference,
"callback_url" => callback_url
}
}
end
end

Now if we run

iex -S mix phx.server
alias ChpterPhoenix.Chpter
Chpter.initiate_payment(                 
"pk_4aff02227456f6b499820c2621ae181c9e35666d25865575fef47622265dcbb9",
"254740769596",
"Michael Munavu",
"michaelmunavu83@gmail.com",
1,
"Nairobi",
"thekultureke.com/api/mpesa_transactions",
"transaction_ref"

)

If you replace the phone number with yours , you should get an stk push .

Now we are good on initiation.

Next , let us add a function to check whether a payment is successful , we will use recursion for this . Add this function in your chpter.ex file

 def check_for_payment(transaction_reference, api_endpoint) do
body = HTTPoison.get!(api_endpoint)

customer_record =
Poison.decode!(body.body)["data"]
|> Enum.find(fn record -> record["transaction_reference"] == transaction_reference end)

customer_record

if customer_record != nil do
customer_record
else
Process.sleep(1000)
check_for_payment(transaction_reference, api_endpoint)
end
end

This function checks to see if there is a record in our api whose transaction reference matches the unique transaction reference we pass when making our initiation and returns its body .

Let us now go to our form component for adding orders and impliment this , we shall edit the save function

We will set it up such that when you click create , you will get a prompt and immediately , we will also start checking for a payment .

alias ChpterPhoenix.Chpter

Add this at the top of lib/chpter_phoenix_web/live/order_live/form_component.ex

So you can have access to the function we wrote there.

Change the save function for a new record to

defp save_order(socket, :new, order_params) do
timestamp =
Timex.local()
|> Timex.format!("{YYYY}{0M}{0D}{h24}{m}{s}")

transaction_reference = order_params["phone_number"] <> timestamp

case Chpter.initiate_payment(
"pk_4aff02227456f6b499820c2621ae181c9e35666d25865575fef47622265dcbb9",
"254740769596",
"Michael Munavu",
"michaelmunavu83@gmail.com",
1,
"Nairobi",
"https://720a-102-135-173-116.ngrok-free.app/api/transactions",
transaction_reference
) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
customer_record =
Chpter.check_for_payment(
transaction_reference,
"http://localhost:4000/api/transactions"
)

if customer_record["success"] == true do
case Orders.create_order(order_params) do
{:ok, _order} ->
{:noreply,
socket
|> put_flash(:info, "Order created successfully")
|> push_redirect(to: socket.assigns.return_to)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
else
{:noreply,
socket
|> put_flash(:error, "Payment Failed , #{customer_record["message"]}")
|> push_redirect(to: socket.assigns.return_to)}
end

{:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
{:noreply,
socket
|> put_flash(:info, "Payment Failed ")}

{:error, %HTTPoison.Error{reason: reason}} ->
{:noreply,
socket
|> put_flash(:info, "Payment Failed , Timeout error")}
end
end

For the callback url , chpter only allows it to be https urls , so we can use ngrok to translate our localhost:4000 to a https url , that is what I use for it .

Let us break down this code , we are initiating payment , we are having case statements to check if the initiation is correct , if it is right , we are then checking if a payment is made , once we get the payment , we see if it is succesful or not , if it is , we create the order .

Now , when we create an order , we see this .

Thank you for reading this article , I beleive we have both learnt a thing or 2 and we can now easily integrate payments in our systems .

--

--