Automating Businesses with WhatsApp Cloud API and Express.js

Collins Hillary

--

Ever wondered how you can automate and extend your business with WhatsApp? Think about it in this way, you have a business, let’s say a jewelry shop, and you want to be able to allow users to reach out to you and order a product while also getting a picking up location and an invoice too!!

With 2 billion monthly active users, now all this is possible with WhatsApp Cloud API accompanied with different deployment methods like QR codes and posters among many.

I assure you it’s much better and interactive than the normal WhatsApp business, it’s a WhatsApp ChatBot.

Objectives

  1. Enable businesses to respond to customers automatically.
  2. Enable users to make inquiries.
  3. Enable users to make orders.
  4. Enable users to get pickup locations near them.
  5. Enable users to get invoices for their transactions.

Assumptions

  1. You are knowledgable with Node.js and Javascript
  2. You(the business) have a valid facebook accountThis is needed because the WhatsApp api is built by Meta(Facebook)
  3. You have installed Ngrok

Getting Started

To get started , I will take you through how to add the WhatsApp Cloud Api on your facebook account.

  1. Once you are logged in here on your account .
  2. Click on Create app
  3. Then click on the Business app type.
  1. Fill in the name of your app and email, then select the page/business you want the app to be associated with.
  1. Then submit

On the screen that will appear after submitting, when you scroll down you will see WhatsApp, click on Set up. This will take you to a new screen where you will see the following. Kindly take note of them…

  1. App ID
  2. Temporary Access Token
  3. Test Phone Number
  4. Phone number ID
  5. WhatsApp Business Account ID

With this already provided, we need to add a phone number that we can use for receiving the text messages which we have a limit of 5 numbers for our development environment.

To add the recipient numbers click the Select a recipient phone number and add your WhatsApp number. If this is the first time, you will get an OTP for phone number confirmation.

To actually make our first test, on the same page there is the send message button. Click on it and you should get a “Hello world…” WhatsApp text message to your number.

Great!! Now we can now move to creating the actual chatbot.

Let’s Cooodddeeeeeeee!!!!!!!

Webhooks

For us to receive the messages from our clients made to our business number(test number) via WhatsApp, we need to add webhooks on the meta dashboard pointing to our code!

Just to note, the codebase for this project can be found here.

Let’s initialise a simple node project by following the commands below inside our folder called social_analytics_saas_backend.

//this initialises node into our project folder
npm init ---yes
//let's install some dependancies
npm install express pdfkit request whatsappcloudapi_wrapper nodemon

Just to mention some dependancies.

pdfkit — for generating our invoices as pdf

request — for making http requests

whatsappcloudapi_wrapper — our WhatsApp api wrapper

nodemon — for continuous server running during development

Next, let’s create the following files and folders:

  1. ./.env.local.js ,,,,,,,,,,file
  2. ./app.js ,,,,,,,,,,file
  3. ./invoices ,,,,,,,,,,folder
  4. ./images ,,,,,,,,,,folder
  5. ./routes/index.js ,,,,,,,,,,file
  6. ./utils/store.js ,,,,,,,,,,file

In the ./.env.local.js file, type the following:

const production = {...process.env,NODE_ENV: process.env.NODE_ENV || 'production',}//from start of the article fill in below Meta valuesconst development = {...process.env,NODE_ENV: process.env.NODE_ENV || 'development',PORT: '9000',Meta_WA_accessToken: '', //temporary access tokenMeta_WA_SenderPhoneNumberId: '', //phone number idMeta_WA_wabaId: '',//business account IDMeta_Wa_VerifyToken: 'setyourrandomtoken', //self defined};const fallback = {...process.env,NODE_ENV: process.env.NODE_ENV || 'fallback',};module.exports =(environment) => {console.log(`Environment: ${environment}`);switch (environment) {case 'production':return production;case 'development':return development;case 'fallback':return fallback;default:return fallback;}}

The code above just sets the values to our environment for easy access.

Next, In the file ./app.js , type the following:

process.env = require('./.env.local.js')(process.env.NODE_ENV || 'development');const port = process.env.PORT || 9000;const express = require('express');let indexRoutes = require('./routes/index.js');const main = async () => {const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false }));app.use('/', indexRoutes);
app.use('*', (req, res) => res.status(404).send('404'));
app.listen(port, () => console.log(`Server started on port ${port}`));}main();

This initialises our node/express app plus injecting the environment variables set before to the express global access object via process.env.

Next, In the file ./routes/index.js, type the following:

'use strict';
const router = require('express').Router();

router.get('/callback', (req, res) => {
try {
console.log("Hey there!")
let mode = req.query['hub.mode'];
let token = req.query['hub.verify_token'];
let challenge = req.query['hub.challenge'];

if (
mode &&
token &&
mode === 'subscribe' &&
process.env.Meta_WA_VerifyToken === token
) {
return res.status(200).send(challenge);
} else {
return res.sendStatus(403);
}
} catch (error) {
console.error({error})
return res.sendStatus(500);
}
});

router.post('/callback', async (req, res) => {
try {
return res.sendStatus(200);
} catch (error) {
console.error({error})
return res.sendStatus(500);
}
});
module.exports = router;

Great, so that we run this so far, edit your package.json file and add a start script as below

{

“name”: “social_analytics_saas_backend”,

“version”: “1.0.0”,

“description”: “”,

“scripts”: {

“start”: “nodemon app.js”, //add this

“test”: “echo \”Error: no test specified\” && exit 1"

},

“keywords”: [],

“author”: “”,

“license”: “ISC”,

“dependencies”: {

“express”: “⁴.18.1”,

“nodemon”: “².0.19”,

“pdfkit”: “⁰.13.0”,

“request”: “².88.2”,

“whatsappcloudapi_wrapper”: “¹.0.13”

}

}

with that done, we can now run below, and the server will start on port 9000

npm start

Also so that facebook(Meta) can reach our server, fire up ngrok like below

ngrok http 9000

Take not of the ngrok url.

Now our webhook is ready for integration with Meta. Go back to the meta dashboard, scroll to under WhatsApp on the side bar and click on Configuration.

Then click on edit under webhook. On the popup add the ngrok url to the callback url field and the Meta_Wa_VerifyToken that we set in our .env file to the verify token field of the popup. The click Verify and Save.

If well configured you should go back to your console and see this

GET: Hey there!

Configuring our server

Now, with that complete, we can make our server receive subscription messages from Meta.

While still on the Meta Dashboard, click on manage and a popup will appear. Select Messages then click Test then Subscribe and then Done

Let’s Get To Business

First, we will need to fetch our store data from somewhere. We will use the FakeStore API, then generate a pdf after order and give back a pickup location with it.

In the file ./utils/store.js.

A lot is happening in this file.

We are importing request and pdfkit libraries that will help us get data from the fake api endpoint and create a pdf with, plus fs library to file system manipulation in javascript.

We then create a simple class that has methods that allow us to get all our products, categories, even products by their unique id. So basically this is a helper class.

After that we then create methods that will assist us in generating the pdf invoice, starting from the generatePDFInvoice function.

Lastly we have the generateRandomLocation function.

Now, type the following:

'use strict';const request = require('request');const PDFDocument = require('pdfkit');const fs = require('fs');module.exports = class Store{constructor(){}async _fetchAssistant(endpoint){return new Promise((resolve, reject) => {request(`https://fakestoreapi.com${endpoint ? endpoint : '/'}`, (err, res, body) => {try {if(err){reject(err);}else{resolve({status:'success',data:JSON.parse(body)});}} catch (error) {reject(error);}});});}async getProductById(productId){return await this._fetchAssistant(`/products/${productId}`);}async getAllCategories(){return await this._fetchAssistant('/products/categories?limit=100');}async getProductsInCategory(categoryId){return await this._fetchAssistant(`/products/category/${categoryId}?limit=100`);}generatePDFInvoice({order_details,file_path}){const doc = new PDFDocument({ margin: 50 });generateHeader(doc);generateCustomerInformation(doc, order_details);generateInvoiceTable(doc, order_details);generateFooter(doc);doc.pipe(fs.createWriteStream(file_path));doc.fontSize(25);doc.end();return;}generateRandomGeoLocation() {let storeLocations = [{latitude: 44.985613,longitude: 20.1568773,address: 'New Castle',},{latitude: 36.929749,longitude: 98.480195,address: 'Glacier Hill',},{latitude: 28.91667,longitude: 30.85,address: 'Buena Vista',},];return storeLocations[Math.floor(Math.random() * storeLocations.length)];}}function generateHeader(doc) {doc.image('./images/logo.png', 50, 65, { width: 90 }).fillColor('#444444').fontSize(20).text('Devligence Limited.', 160, 65).fontSize(10).text('Nairobi, Kenya', 200, 65, { align: 'right' }).text('Nairobi, KE, 10025', 200, 80, { align: 'right' }).moveDown();}function generateCustomerInformation(doc, order_details) {console.log(order_details,100,100)doc.fillColor('#444444').fontSize(20).text('Invoice', 50, 160);generateHr(doc,185);const customerInformationTop = 200;doc.fontSize(10).text("Invoice Number:", 50, customerInformationTop).font("Helvetica-Bold").text(order_details.invoice_nr, 150, customerInformationTop).font("Helvetica").text("Invoice Date:", 50, customerInformationTop + 15).text(formatDate(new Date()), 150, customerInformationTop + 15)generateHr(doc,252);}function generateInvoiceTable(doc, invoice) {let i,invoiceTableTop = 330;doc.font("Helvetica-Bold");generateTableRow(doc,invoiceTableTop,"Item","Unit Cost","Quantity","Line Total");generateHr(doc, invoiceTableTop + 20);doc.font("Helvetica");for (i = 0; i < invoice.items.length; i++) {const item = invoice.items[i];const position = invoiceTableTop + (i + 1) * 30;generateTableRow(doc,position,item.name,formatCurrency(item.price),item.quantity,formatCurrency(item.quantity * item.price));generateHr(doc,position + 20);}const subtotalPosition = invoiceTableTop + (i + 1) * 30;generateTableRow(doc,subtotalPosition,"","","Subtotal","",formatCurrency(invoice.subtotal));const paidToDatePosition = subtotalPosition + 20;generateTableRow(doc,paidToDatePosition,"","","Paid To Date","",formatCurrency(invoice.paid));const duePosition = paidToDatePosition + 25;doc.font("Helvetica-Bold");generateTableRow(doc,duePosition,"","","Balance Due","",formatCurrency(invoice.subtotal - invoice.paid));doc.font("Helvetica");}function generateFooter(doc) {doc.fontSize(10,).text('Payment is due within 15 days. Thank you for your business.',50,680,{ align: 'center', width: 500 },);}function generateTableRow(doc,y,item,unitCost,quantity,lineTotal) {doc.fontSize(10).text(item, 50, y).text(unitCost, 280, y, { width: 90, align: "right" }).text(quantity, 370, y, { width: 90, align: "right" }).text(lineTotal, 0, y, { align: "right" });}function generateHr(doc, y) {doc.strokeColor('#aaaaaa').lineWidth(1).moveTo(50, y).lineTo(550, y).stroke();}function formatCurrency(cents) {return "$" + (cents / 100).toFixed(2);}function formatDate(date) {const day = date.getDate();const month = date.getMonth() + 1;const year = date.getFullYear();return year + "/" + month + "/" + day;}

Great, it’s not too complex init?

Next, Sessions

We would want to have every client to have their own session, this will allow our clients to have an add to cart functionality on their WhatsApp chat!!

Now this would be done in production by storing user details in a database, for persistent storage but for this article we will use ES2015’s Map data structure.

Using Javascript Map, we can store and retrieve specific data such as data associated to a client.

Let’s get into it….

In the ./routes/index.js file, type the following under the post callback. To Note: During WhatsApp conversations with our chatbot, the responses will be captured in our post callback.

Start with importing our file under utils folder, initialising it and creating a Map object as below

....existing codeconst Store = require('../utils/store');const Store = new Store();const CustomerSession = new Map();....existing code

Connecting to our WhatsApp Wrapper

To now start capturing messages and creating the conversations, we will add our wrapper now. as below still inside our index.js file.

Type the following under the imports, remember the process.env variables we already set in our .env.local file

const WhatsappCloudAPI = require('whatsappcloudapi_wrapper');
const Whatsapp = new WhatsappCloudAPI({
accessToken: process.env.Meta_WA_accessToken,
senderPhoneNumberId: process.env.Meta_WA_SenderPhoneNumberId,
WABA_ID: process.env.Meta_WA_wabaId,
});

Now the final code below, will handle the following logic:

  1. We will add the message object received by the client to the data variable by parsing it using our wrapper library.
  2. Then if the message object is valid, we will extract details such as the actual message, the recipient phone, recipient name , the type of message and the unique message id. for later use through the process.
  3. We need to initiate the customer session, so we map the customer session with the recipients phone number.
  4. we then add an addToCart function and listOfItemsInCart plus clearCart functionality that basically manipulates the customer session store.
  5. Now, we do know we need to structure the conversations with the clients by using AI or natural language processing, but for this demo, we will use simple if…else statements.
  6. The conversation logic starts when the customer sends a text message. We won’t look at the message itself, so we won’t know what they intended to do, but we can tell the customer what our bot can do.
  7. Let’s give our customer a simple context, to which they can reply with a specific intent.
  8. The type of messages can be different but for our app we will check only text_message, simple_button_message, radio_button_message.
  9. We then respond accordingly.
  10. Finally we add the blue tick effect using the markMessageAsRead function.

So our final index.js would look like below:

'use strict';const router = require('express').Router();const Store = require('../utils/store');const Store = new Store();const CustomerSession = new Map();
const WhatsappCloudAPI = require('whatsappcloudapi_wrapper');const Whatsapp = new WhatsappCloudAPI({accessToken: process.env.Meta_WA_accessToken,senderPhoneNumberId: process.env.Meta_WA_SenderPhoneNumberId,WABA_ID: process.env.Meta_WA_wabaId,})router.get('/callback', (req, res) => {try{let mode = req.query['hub.mode'];let token = req.query['hub.verify_token'];let challenge = req.query['hub.challenge'];if(mode && token && challenge && mode === 'subscribe' && process.env.Meta_Wa_VerifyToken === token){console.log("Get: I am verified!");res.status(200).send(challenge);}else{console.log("Get: I am not verified!");res.status(403).send('Error, wrong token');}}catch(err){console.log(err);res.status(500).send(err);}})router.post('/callback',async (req, res) => {try{let data = Whatsapp.parseMessage(req.body);if(data?.isMessage){let incomingMessage = data.message;let recipientPhone = incomingMessage.from.phone; // extract the phone number of senderlet recipientName = incomingMessage.from.name;let typeOfMsg = incomingMessage.type; // extract the type of message (some are text, others are images, others are responses to buttons etc...)let message_id = incomingMessage.message_id; // extract the message id// Start of cart logicif (!CustomerSession.get(recipientPhone)) {CustomerSession.set(recipientPhone, {cart: [],});}let addToCart = async ({ product_id, recipientPhone }) => {let product = await Store.getProductById(product_id);if (product.status === 'success') {CustomerSession.get(recipientPhone).cart.push(product.data);}};let listOfItemsInCart = ({ recipientPhone }) => {let total = 0;let products = CustomerSession.get(recipientPhone).cart;total = products.reduce((acc, product) => acc + product.price,total);let count = products.length;return { total, products, count };};let clearCart = ({ recipientPhone }) => {CustomerSession.get(recipientPhone).cart = [];};// End of cart logicif(typeOfMsg === 'text_message'){await Whatsapp.sendSimpleButtons({message: 'Hello ' + recipientName + ' \nYou are speaking to a chatbot.\nWhat do you want to do next?',recipientPhone: recipientPhone,listOfButtons: [{title: 'View some products',id:'see_categories',},{title:'Speak to a human',id:'speak_to_human',}]})}if(typeOfMsg === 'simple_button_message'){let button_id = incomingMessage.button_reply.id;if(button_id === 'speak_to_human'){await Whatsapp.sendText({recipientPhone: recipientPhone,message:`Arguably, chatbots are faster than humans.\nCall my human with the below details:`});await Whatsapp.sendContact({recipientPhone: recipientPhone,name: 'Human',contact_profile:{addresses:[{city:'Nairobi',country:'Kenya',},],name: {first_name: 'Collins',last_name: 'Hillary',},org: {company: 'Shop',},phones: [{phone: '+1 (555) 025-3483',},{phone: '+254712345678',},],}})}if(button_id === 'see_categories'){let categories = await Store.getAllCategories();await Whatsapp.sendSimpleButtons({message: 'We have several categories.\nChoose one of them.',recipientPhone: recipientPhone,listOfButtons: categories.data.map((category) => ({title: category,id: `category_${category}`,})).slice(0,3)})}if(button_id.startsWith('category_')){let selectedCategory = button_id.split('category_')[1];let listOfProducts = await Store.getProductsInCategory(selectedCategory);let listOfSections = [{title: `🏆 Top 3: ${selectedCategory}`.substring(0,24),rows: listOfProducts.data.slice(0,3).map((product) => {let id = `product_${product.id}`.substring(0,256);let title = product.title.substring(0,21);let description = `${product.price}\n${product.description}`.substring(0,68);return {id,title: `${title}...`,description: `$${description}...`};}).slice(0,10)}];await Whatsapp.sendRadioButtons({recipientPhone: recipientPhone,headerText: `#BlackFriday Offers: ${selectedCategory}`,bodyText: `Our Santa 🎅🏿 has lined up some great products for you based on your previous shopping history.\n\nPlease select one of the products below:`,footerText: 'Powered by: BMI LLC',listOfSections,});}if (button_id.startsWith('add_to_cart_')) {let product_id = button_id.split('add_to_cart_')[1];await addToCart({ recipientPhone, product_id });let numberOfItemsInCart = listOfItemsInCart({ recipientPhone }).count;await Whatsapp.sendSimpleButtons({message: `Your cart has been updated.\nNumber of items in cart: ${numberOfItemsInCart}.\n\nWhat do you want to do next?`,recipientPhone: recipientPhone,listOfButtons: [{title: 'Checkout 🛍️',id: `checkout`,},{title: 'See more products',id: 'see_categories',},],});}if (button_id === 'checkout') {let finalBill = listOfItemsInCart({ recipientPhone });let invoice_data = {}invoice_data.date = new Date().toISOString();invoice_data.time = new Date().toLocaleTimeString();invoice_data.customer = {name: recipientName,phone: recipientPhone,};invoice_data.items = finalBill.products.map((product) => {return {name: product.title,price: product.price,quantity: 1,};});invoice_data.invoice_nr = Math.floor(Math.random() * 1000000);Store.generatePDFInvoice({order_details: invoice_data,file_path: `./invoices/invoice_${recipientName}.pdf`,});await Whatsapp.sendSimpleButtons({recipientPhone: recipientPhone,message: `Thank you for shopping with us, ${recipientName}.\n\nYour order has been placed and invoice has been generated.`,message_id,listOfButtons: [{title: 'See more products',id: 'see_categories',},{title: 'Print my invoice',id: 'print_invoice',},],});clearCart({ recipientPhone });}if (button_id === 'print_invoice') {// Send the PDF invoiceawait Whatsapp.sendDocument({recipientPhone: recipientPhone,caption:`Shop invoice #${recipientName}`,file_path: `./invoices/invoice_${recipientName}.pdf`,});// Send the location of our pickup station to the customer, so they can come and pick up their orderlet warehouse = Store.generateRandomGeoLocation();await Whatsapp.sendText({recipientPhone: recipientPhone,message: `Your order has been fulfilled. Come and pick it up, as you pay, here:`,});await Whatsapp.sendLocation({recipientPhone,latitude: warehouse.latitude,longitude: warehouse.longitude,address: warehouse.address,name: 'Shop',});}}if(typeOfMsg === 'radio_button_message'){let selectionId = incomingMessage.list_reply.id;if (selectionId.startsWith('product_')) {let product_id = selectionId.split('_')[1];let product = await Store.getProductById(product_id);const { price, title, description, category, image: imageUrl, rating } = product.data;let emojiRating = (rvalue) => {rvalue = Math.floor(rvalue || 0); // generate as many star emojis as whole number ratingslet output = [];for (var i = 0; i < rvalue; i++) output.push('⭐');return output.length ? output.join('') : 'N/A';};let text = `_Title_: *${title.trim()}*\n\n\n`;text += `_Description_: ${description.trim()}\n\n\n`;text += `_Price_: $${price}\n`;text += `_Category_: ${category}\n`;text += `${rating?.count || 0} shoppers liked this product.\n`;text += `_Rated_: ${emojiRating(rating?.rate)}\n`;await Whatsapp.sendImage({recipientPhone,url: imageUrl,caption: text,});await Whatsapp.sendSimpleButtons({message: `Here is the product, what do you want to do next?`,recipientPhone: recipientPhone,listOfButtons: [{title: 'Add to cart🛒',id: `add_to_cart_${product_id}`,},{title: 'Speak to a human',id: 'speak_to_human',},{title: 'See more products',id: 'see_categories',},],});}}await Whatsapp.markMessageAsRead({ message_id });}res.status(200).send('OK');}catch(err){console.log(err);res.status(500).send(err);}});module.exports = router;

Of course, a lot of functionalities can be added to this. I leave you to build up on this article and project.

Below are screenshots of the interaction on the WhatsApp application:

Interaction images

That’s it, hope everything was clear and now you can be able to use WhatsApp cloud api to create a basic chatbot. If not, don’t hesitate to reach out.

Happy Coding.

More resources:::

Yellow.ai

Voiceui

Author: Collins Munene

Github:https://github.com/CollinsMunene

Email: collinshillary1@gmail.com

--

--