Introduction
Hey folks! I am Kizito a Software Engineer at Microsofzz(wakes up). In this article I will be guiding you through developing API endpoints for payment integration on Paystack π which can be used for your client side applications.
If you seek to monetize your applications with Paystack using Node.js seek no further, for you are in the right place π€©
Prerequisites
- Basic Knowledge of Javascript, Node.js and Express
- You should have VS Code, Node.js, MongoDb and Postman Installed.
- Experience in working with Databases (I'll be using MongoDb for this article).
- A heart willing to learn β€οΈβπ₯
Let's dive in
In today's digital age, where e-commerce is booming and online transactions have become the norm, building a seamless and secure payment system is paramount for businesses and developers alike. But building a payment system can be a daunting task, and that's where Paystack comes into play. Paystack, a widely trusted payment gateway, provides developers with the tools they need to handle payments effortlessly. In this comprehensive guide, we'll embark on a journey to demystify the process of building a cutting-edge payment system in Node.js using Paystack. So, fasten your seatbelts, as we embark on this exciting journey into the world of Node.js and Paystack-powered payment systems.
Setup
If you do not have a paystack account you'll need to start by creating one here https://dashboard.paystack.com/#/signup if you already have one then you can proceed to login.
Having done that we can proceed to development π©βπ»π¨βπ»
I will be making use of couple of the most popular endpoints in this article which will be the create charge endpoints and the subscription endpoints π₯π₯
Open up your VS Code and create a new folder directly from your terminal as a senior dev π«‘. I'll be calling mine paystack-nodejs-integration
Then you cd into that directory using
mkdir paystack-nodejs-integration
cd paystack-nodejs-integration
Now that we are in that same directory we do
npm init -y
and we see our package.json file opens up π
Next up let us install our dependencies.
These are the dependencies that we are going to use to develop this awesome system and I'll shed light π‘ on them one after another.
Then we create our index.js, env and gitignore file.
npm install cors dotenv express mongoose paystack-api
npm install --save-dev nodemon @ngrok/ngrok
touch index.js .env .gitignore
As stated, I will shed light on each of the dependencies.
cors: CORS is a node.js package for providing a Connect/Express middleware that can be used to enable cross-origin-resource-sharing with various options for access to our API.
dotenv: Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology and we are going to use it to store our API Secret Keys.
express: This is the Nodejs framework we will use to build our API.
mongoose: Mongoose is the tool that makes it easier for us to work directly and interact with our MongoDb.
paystack-api: The Pariola paystack api wrapper which does all the paystack under the hood connections in our nodejs app and provides functions for our integration.
nodemon: nodemon is a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected.
ngrok: ngrok delivers instant ingress to your apps in
any cloud, private network, or devices
with authentication, load balancing, and other critical controls. We will be using it for our webhook event testing to connect our localhost to a live URL π₯
You can create an account here Ngrok
Nodemon and Ngrok were installed as dev dependencies because we wouldn't be needing them live unless you want to use some of ngrok's additional features.
Sooo our folder structure should look similar to this and now we startup our express server in our index.js file.
index.js
// imports
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
// specify the port from our enviroment variable
const PORT = process.env.PORT || 8080;
app.use(cors());
app.use(express.json());
// connect database
// routes
app.get('/', async (req, res) => {
res.send("Hello World!");
});
app.listen(PORT, () => {
console.log(
`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`
);
});
.env
PORT=8000
NODE_ENV=development
.gitignore
node_modules
.env
package.json
{
"name": "paystack-nodejs-integration",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.5.0",
"paystack-api": "^2.0.6"
},
"devDependencies": {
"@ngrok/ngrok": "^0.6.0",
"nodemon": "^3.0.1"
}
}
Notice the scripts in the json file. We will be using
on the terminal for our development mode.
npm run dev
Then we try the url on postman
Who dey check! (exclaims in joy)
Now let us connect to our MongoDb Compass.
Then we set our MONGODB_URI in our .env file
MONGODB_URI=mongodb://0.0.0.0:27017/paystack-node-integration
After that, we add our MongoDb configuration in the config/db.js file and then we save
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Then we import and call the function in our index.js
// imports
const connectDB = require("./config/db.js");
// connect database
connectDB();
Boom! Our database is connected
Now let us create a system where we have users and they can donate for a campaign using their cards and can also be able to subscribe to a plan you created.
Let us start by creating the user model for our database.
We create a folder called models and in the folder, we create a file called userModel.js.
models/userModel.js
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
fullname: {
type: String,
},
email: {
type: mongoose.Schema.Types.Mixed,
},
paystack_ref: {
type: String,
},
amountDonated: {
type: Number,
},
isSubscribed: {
type: Boolean,
},
planName: {
type: String,
},
timeSubscribed: {
type: Date,
},
});
const User = mongoose.model("user", userSchema);
module.exports = User;
We are going to be using the common MVC (Models, Views, Controller) architectural pattern except ours will be routes endpoints instead of views.
So let's create two(2) extra folders controllers and routes to make our app easy to debug, more scalable and to be understood by other developers unless, you go explain tire (you will over explain).
We want to create two endpoints for now. The first will be to create a user and the second will be to get the user.
In our controllers we create a new file called userController.js
controllers/userController.js
const User = require("../models/userModel");
// Require paystack library
const createUser = async (req, res) => {
let { email, fullname } = req.body;
const user = new User({
fullname,
email,
});
await user.save();
res.status(201).send({
data: user,
message: "User created successfully",
status: 0,
});
}
const getUser = async (req, res) => {
try {
let { id } = req.params;
const user = await User.findById(id);
res.status(200).send({
user,
message: "Found user Details",
status: 0,
});
} catch (err) {
res.status(500).send({ data: {}, error: err.message, status: 1 });
}
};
// initialize transaction
module.exports = {
createUser,
getUser,
};
After that, we create another folder named routes and create a file called userRoutes.js which will be used to call our controller functions.
routes/userRoutes.js
const express = require("express");
const userRoute = express.Router();
const {
getUser,
createUser,
} = require("../controllers/userController");
userRoute.get("/getUser/:id", getUser);
userRoute.post("/createUser", createUser);
module.exports = {
userRoute,
};
In order to be able to use the routes, we have to import it in our index.js file.
// routes
const { userRoute } = require("./routes/userRoutes.js");
app.use("/users", userRoute);
So far so good! Our folder structure should look like this.
Now we test our user creation endpoint on postman and see that it works and it reflects in our database π
Congratulations on making it this farππ I guess we both deserve a drink π»
This is the moment we all have been waiting for.
Moving on to the next phase, we remember that we want to create a system where we have users and they can donate for a campaign and can also be able to subscribe to a plan you created.
These are two key implementations
- Campaign donations
- Plan Subscriptions
Campaign Donations
Imagine if we are organizing an event and we need people to monetize by donating some funds, let us create an API endpoint for this.
Before that let's go to paystacks dashboard settings and copy our secret key in order for us to be able to interact with our account. https://dashboard.paystack.com/#/settings/developers
And for this article purposes I will be on test mode so no real funds are going to be transacted.
β οΈRemember, on no account should you share your secret key to anyone that is why it is going to be stored in an environmental variable which will be omitted on any deployment. In case of any public display which was done mistakenly you should generate a new key. β οΈ
Adding a new line to our .env file where we copy and paste our secret key there.
.env
TEST_SECRET=YOUR_SECRET_KEY
In our controllers/userController.js we add the following lines to initialize a transaction and we save the transaction reference to get the details about a particular transaction which you must have come across in your regular banking transactions.
https://paystack.com/docs/api/charge/#create
controllers/userController.js
// Require paystack library
const paystack = require("paystack-api")(process.env.TEST_SECRET);
// initialize transaction
const initializeTrans = async (req, res) => {
try {
let { id } = req.params;
const { email, amount, plan, } = req.body;
const response = await paystack.transaction.initialize({
email,
amount,
plan, // optional but we'll use for subscription
});
const data = {
paystack_ref: response.data.reference,
};
await User.findByIdAndUpdate(id, data);
res.status(200).send({
data: response.data,
message: response.message,
status: response.status,
});
} catch (error) {
res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
}
};
// verify transaction
module.exports = {
...,
initializeTrans,
};
If you look at the code above you notice we have 3 inputs taken into the body parameters.
email, amount and plan(which is optional but since we are also going to have subscriptions we will be using it later.
Let's add it to our userRoute
routes/userRoutes.js
const {
...,
initializeTrans,
} = require("../controllers/userController");
userRoute.post("/initiatetransaction/:id", initializeTrans);
Yes! time to test on postman.
I set the amount to be 300000 which is 3000 due to currency two decimal places and the email to be mine as payer.
It works π
We are going to click on the authorization_url and see what happens.
Bravo friends πͺ we can see our payment options provided and proceed to payment.
After payment, we want to verify that the payment was successful and we can achieve that in two methods.
- Using the paystack verify endpoint https://paystack.com/docs/payments/verify-payments/
- Using webhooks to listen for events https://paystack.com/docs/payments/webhooks/
In this article we will be covering this two methods π₯
Let's start by verifying the transaction we just made with the verify endpoint. On verification, we add the amount the user donated and change the transaction reference to success in our database.
controllers/userController.js
// verify transaction
const verifyTrans = async (req, res) => {
try {
let { id } = req.params;
const user = await User.findById(id);
if (user.paystack_ref == "success")
return res.status(401).send({
data: {},
message: "Transaction has been verified",
status: 1,
});
const response = await paystack.transaction.verify({
reference: user.paystack_ref
});
if (response.data.status == "success") {
const data = {
paystack_ref: response.data.status,
amountDonated: response.data.amount,
};
await User.findByIdAndUpdate(id, data);
return res
.status(200)
.send({
data: response.data,
message: response.message,
status: response.status,
});
} else {
return res
.status(200)
.send({
data: response.data,
message: response.message,
status: response.status,
});
}
} catch (error) {
res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
}
};
module.exports = {
...,
verifyTrans,
};
routes/userRoutes.js
const {
...,
verifyTrans,
} = require("../controllers/userController");
userRoute.post("/verifytransaction/:id", verifyTrans);
After testing the endpoint on postman, we can verify it works from our response and in our database π€©π€©
Yay! Now we are sure this works, we can go ahead to work on the Plan Subscriptions and over here, we will get to see how we can use webhooks for event listening ππͺ
Plan Subscriptions
In a scenario whereby you are the CEO of a kitchen app and you want people to be able to subscribe to give them access to various chefs on the app you have to create several plans for them which can either be weekly or monthly and so on. Let's create these plans and enable subscription on our app.
We can also create plans directly from our paystack dashboard but I will be doing so programatically.
We create a new file in our controllers and call it planController.js where we are going to create our createPlan, getPlan and webhook function.
controllers/planController.js
// Require the library
const paystack = require("paystack-api")(process.env.TEST_SECRET);
const createPlan = async (req, res) => {
try {
const { interval, name, amount } = req.body;
const response = await paystack.plan.create({
name,
amount,
interval,
});
res.status(200).send({
data: response.data,
message: response.message,
status: response.status,
});
} catch (error) {
res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
}
};
const getPlans = async (req, res) => {
try {
const response = await paystack.plan.list();
res.status(200).send({
data: response.data,
message: response.message,
status: response.status,
});
} catch (error) {
res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
}
};
// our webhook function for event listening
module.exports = {
createPlan,
getPlans,
};
We create a new file in our routes call planRoutes.js
routes/planRoutes.js
const express = require("express");
const planRoute = express.Router();
const {
createPlan,
getPlans,
} = require("../controllers/planController");
planRoute.get("/getPlans", getPlans);
planRoute.post("/createPlan", createPlan);
module.exports = {
planRoute,
};
Let's also not forget to import it in our index.js
index.js
// routes
const { planRoute } = require("./routes/planRoutes.js");
app.use("/plans", planRoute);
Saving our file and testing our createPlan endpoint, we see that it works π
We have successfully created a plan for our users to subscribe to π»
What next...? Yes you are right, we have to subscribe to the plan but before we do that, we are going to add our webhook functions to listen to transaction events sent by paystack.
So we create a new folder called helpers and there we create a file called webhookHelpers.js
In this file we have 3 functions that will be triggered when an event is updated chargeSuccess, planChargeSuccess and cancelSubscription. Depending on your choice of usage you can create more.
https://paystack.com/docs/payments/subscriptions/
helpers/webhookHelpers.js
const User = require("../models/userModel");
// Require the library
const paystack = require("paystack-api")(process.env.TEST_SECRET);
// Paystack webhook helpers: Functions that should be called on paystack event updates
// invoicePaymentFailed, invoiceCreation, invoiceUpdate, subscriptionNotRenewed, subscriptionDisabled, chargeSuccess
const chargeSuccess = async (data) => {
try {
const output = data.data;
const reference = output.reference;
// console.log(output);
const user = await User.findOne({ paystack_ref: reference });
const userId = user._id;
console.log("Updating charge status");
if (user.paystack_ref == "success")
return ({
data: {},
message: "Transaction has been verified",
status: 1,
});
const response = await paystack.transaction.verify({
reference: user.paystack_ref
})
if (response.data.status == "success") {
const data = {
paystack_ref: response.data.status,
amountDonated: output.amount,
}
await User.findByIdAndUpdate(userId, data);
console.log("Charge Successful");
} else {
console.log("Charge Unsuccessful");
}
} catch (error) {
console.log({ data: {}, error: `${error.message}`, status: 1 });
}
};
// succesful subscription
const planChargeSuccess = async (data) => {
try {
const output = data.data;
const reference = output.reference;
// console.log(output);
const user = await User.findOne({ paystack_ref: reference });
const userId = user._id;
// console.log(user, reference);
console.log("Updating charge status");
// subscribe for user
if (user.paystack_ref == "success")
return ({
data: {},
message: "Transaction has been verified",
status: 1,
});
const response = await paystack.transaction.verify({
reference: user.paystack_ref
})
if (response.data.status == "success") {
await User.findByIdAndUpdate(userId, {
isSubscribed: true,
paystack_ref: response.data.status,
planName: output.plan.name,
timeSubscribed: response.data.paid_at,
});
console.log("Charge Successful");
} else {
console.log("Charge Unsuccessful");
}
} catch (error) {
console.log({ data: {}, error: `${error.message}`, status: 1 });
}
};
// invoicePaymentFailed
const cancelSubscription = async (data) => {
try {
const output = data.data;
const reference = output.reference;
// console.log(output);
const user = await User.findOne({ paystack_ref: reference });
const userId = user._id;
console.log("Cancelling subscription...");
await User.findByIdAndUpdate(userId, {
isSubscribed: true,
paystack_ref: response.data.status,
planName: "cancelled",
});
console.log("User Subscription Cancelled");
} catch (error) {
console.log({ data: {}, error: `${error.message}`, status: 1 });
}
};
module.exports = {
planChargeSuccess,
chargeSuccess,
cancelSubscription,
};
In our planController we import the helper functions
Once an action like a successful charge is created it triggers an event which we are going to select an endpoint URL for paystack to send the data to.
controllers/planController.js
// Require the library
const { planChargeSuccess, chargeSuccess, cancelSubscription, } = require("../helpers/webhookHelpers");
// our webhook function for event listening
// you can edit this to your style
const addWebhook = async (req, res) => {
try {
let data = req.body;
console.log('Webhook data: ', data);
switch (data) {
case data.event = "invoice.payment_failed":
await cancelSubscription(data);
console.log("Invoice Failed");
break;
case data.event = "invoice.create":
console.log("invoice created");
break;
case data.event = "invoice.update":
data.data.status == "success" ?
await planChargeSuccess(data) :
console.log("Update Failed");
break;
case data.event = "subscription.not_renew":
console.log("unrenewed");
break;
case data.event = "subscription.disable":
console.log("disabled");
break;
case data.event = "transfer.success":
console.log("transfer successful");
break;
case data.event = "transfer.failed":
console.log("transfer failed");
break;
case data.event = "transfer.reversed":
console.log("transfer reversed");
break;
case data.event = "subscription.disable":
console.log("disabled");
break;
default:
// successful charge
const obj = data.data.plan;
console.log("Implementing charges logic...");
// object comparison verifying if its a normal payment or a plan
// charges for subscription and card
Object.keys(obj).length === 0 && obj.constructor === Object ?
await chargeSuccess(data) :
// charge sub
await planChargeSuccess(data);
console.log("Successful");
break;
}
} catch (error) {
res.status(400).send({ data: {}, error: `${error.message}`, status: 1 });
}
};
module.exports = {
...,
addWebhook,
};
We are going to call it here in the planRoutes
const {
...,
addWebhook,
} = require("../controllers/planController");
planRoute.post("/paystackWebhook", addWebhook);
To us who made it this far we deserve pounded yam, don't we? π
One final push!
Before we begin trying out our endpoint we need to use our ngrok tool to deploy our localhost live so we can add the url to paystack.
https://dashboard.paystack.com/#/settings/developers
Then we login to https://dashboard.ngrok.com/get-started/your-authtoken for our token, copy it and add it to our .env file
.env
NGROK_AUTHTOKEN=YOUR_AUTH_TOKEN
We add the following lines of code to our index.js
index.js
// imports
const ngrok = require("@ngrok/ngrok");
// at the bottom
if (process.env.NODE_ENV == "development") {
(async function () {
const url = await ngrok.connect({ addr: PORT, authtoken_from_env: true, authtoken: process.env.NGROK_AUTHTOKEN });
console.log(`Ingress established at: ${url}`);
})();
}
Then we save and our terminal should look similar to this and if we click on the link we should see our Hello World!
Let's add the link to our test webhook URL on paystack so our events can be triggered. Your link should be different.
https://ce41-102-88-63-21.ngrok-free.app/plans/paystackWebhook
Note: Each time nodemon restarts our server, the webhook URL changes.
Great! so we test with the initiatetransaction endpoint on postman but notice this time we added a plan to the body and that amount will be overridden by the plan amount we created.
Yay π₯³π It works smoothly π€©
Now we can stand up and stretch our body... Feels good π
Summary
We have learnt and sharpened up lot from working with paystack transactions, REST APIs, modelling our database, building with a solid architecture, working with tools and libraries like nodemon, dotenv, mongoose, ngrok etc. and we have seen an importance of webhooks and event in developing real world applications and even cases of initializing payment process for a contribution or a subscription plan.
Paystack also has a lot more features you can explore in their documentation.
If you encounter any challenge, feel free to comment here or connect with me on LinkedIn or Twitter
Congratulations once again π
Thanks for coming all the way and I wish you success in your endeavours.
Top comments (16)
Nice write-up bro ππ½.
Your article popped up on my Google Feed. The cover image (Yuji) piqued my interest π.
Thank you so much.
(Myy Brother) ππ
Haha, nice one π. You're welcome (my brother).
Thank you!!
Clear and concise, thanks for cooking! π€
Thanks bro π
Still making more recipes π
This article was sleek.
Nice Read
Thanks.
That's how we roll π
This article was really useful
Glad you found it useful π
Great write up man
Thanks bro πͺ
Wow,this was really really helpful. Thanks a lot manππ
Glad it helped... I wish you a Happy December π»
I donβt see the point in creating something from scratch when there are already more than secure, reliable, and most importantly, easy-to-integrate payment solutions. For example, Payopβs payop.com/en payment api would be an excellent, optimized solution with a wide range of payment methods, both traditional and alternative. This api supports multiple currencies, allowing businesses to work with customers worldwide.
This is a great piece. In fact, it is the best tutorial I have come across online, on Integrating paystack in nodejs. If I am to rate this piece out of 10, I would give it a 9/10.
What we have been able to do in this tutorial, with respect to cancellation of a subscription, is to await an event (e.g. invoice.payment_failed) that would then trigger our "cancelSubscription" function.
What if I want to cancel by myself, how do i go about it, so that when I do, my apo communicates the cancellation to paystack, so that I don't get billed during the next billing cycle?