Skip to main content

Build a WhatsApp Chatbot

Create an intelligent WhatsApp chatbot that can handle customer inquiries, process orders, and provide automated support.

What You'll Build​

In this tutorial, you'll create a WhatsApp chatbot that:

  • ✅ Responds to incoming WhatsApp messages automatically
  • ✅ Handles menu-based interactions (order status, FAQs, support)
  • ✅ Processes customer inquiries with AI (optional)
  • ✅ Sends rich media (images, documents, buttons)
  • ✅ Uses webhooks for real-time message handling

Estimated Time: 45 minutes

Prerequisites​

Before you begin, ensure you have:

  • ✅ AfriRoute Account with WhatsApp API access (Sign up)
  • ✅ WhatsApp Business Account approved by Meta
  • ✅ Node.js 18+ or Python 3.8+ installed
  • ✅ ngrok or similar tunnel for webhook testing
  • ✅ Basic understanding of REST APIs and webhooks

Architecture Overview​

┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│ Customer │─────────│ WhatsApp │─────────│ AfriRoute │
│ (WhatsApp) │ │ Business │ │ Platform │
└──────────────┘ │ API │ └──────────────┘
└──────────────┘ │
│ Webhook
▼
┌──────────────┐
│ Your Bot │
│ Server │
└──────────────┘

Step 1: Set Up Your Project​

Install Dependencies​

Node.js:

mkdir whatsapp-chatbot
cd whatsapp-chatbot
npm init -y
npm install express afriroute-sdk body-parser dotenv

Python:

mkdir whatsapp-chatbot
cd whatsapp-chatbot
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install afriroute-python flask python-dotenv

Create Environment File​

.env
AFRIROUTE_API_KEY=$AFRIROUTE_API_KEY
AFRIROUTE_API_SECRET=your_api_secret_here
WEBHOOK_SECRET=your_webhook_secret_here
PORT=3000
Security

Never commit .env to version control! Add it to .gitignore

Step 2: Create the Bot Server​

Node.js Implementation​

bot.js
const express = require('express');
const { AfriRoute } = require('afriroute-sdk');
require('dotenv').config();

const app = express();
app.use(express.json());

// Initialize AfriRoute client
const afriroute = new AfriRoute({
apiKey: process.env.AFRIROUTE_API_KEY,
apiSecret: process.env.AFRIROUTE_API_SECRET
});

// Store conversation states (use Redis in production)
const conversationStates = new Map();

// Bot menu structure
const MENU = {
main: {
text: `👋 Welcome to AfriShop!

Please select an option:
1️⃣ View Products
2️⃣ Check Order Status
3️⃣ FAQs
4️⃣ Contact Support
5️⃣ About Us`,
options: ['1', '2', '3', '4', '5']
},
products: {
text: `📦 Our Products:

1. Electronics 💻
2. Fashion 👗
3. Home & Garden �
4. Sports 🎾

Reply with a number or type "back" for main menu`,
options: ['1', '2', '3', '4', 'back']
}
};

// Handle incoming webhooks
app.post('/webhook/whatsapp', async (req, res) => {
try {
const { event, message } = req.body;

// Verify webhook signature
const signature = req.headers['x-afriroute-signature'];
if (!verifyWebhook(signature, req.body)) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Only process incoming messages
if (event === 'message.received' && message.direction === 'inbound') {
await handleIncomingMessage(message);
}

res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Process incoming messages
async function handleIncomingMessage(message) {
const { from, text, messageId } = message;
const userInput = text.body.toLowerCase().trim();

// Get or create conversation state
let state = conversationStates.get(from) || { stage: 'main', data: {} };

let response;

switch (state.stage) {
case 'main':
response = await handleMainMenu(from, userInput, state);
break;

case 'products':
response = await handleProducts(from, userInput, state);
break;

case 'order_tracking':
response = await handleOrderTracking(from, userInput, state);
break;

case 'support':
response = await handleSupport(from, userInput, state);
break;

default:
response = MENU.main.text;
state.stage = 'main';
}

// Update conversation state
conversationStates.set(from, state);

// Send response
await sendWhatsAppMessage(from, response);
}

// Handle main menu
async function handleMainMenu(from, input, state) {
switch (input) {
case '1':
state.stage = 'products';
return MENU.products.text;

case '2':
state.stage = 'order_tracking';
return '📦 Please enter your order number (e.g., ORD-12345):';

case '3':
return `❓ Frequently Asked Questions:

1. How do I track my order?
2. What's your return policy?
3. Payment methods
4. Delivery time

Reply with a number`;

case '4':
state.stage = 'support';
return '🤝 How can we help you? Please describe your issue:';

case '5':
return `ℹ️ About AfriShop

We're Africa's leading online marketplace offering quality products with fast delivery.

🌍 Serving 8+ countries
📦 100K+ products
⭐ 4.8/5 customer rating

Visit: www.afrishop.com`;

default:
return MENU.main.text;
}
}

// Handle product browsing
async function handleProducts(from, input, state) {
if (input === 'back') {
state.stage = 'main';
return MENU.main.text;
}

const categories = {
'1': { name: 'Electronics', emoji: '💻' },
'2': { name: 'Fashion', emoji: '👗' },
'3': { name: 'Home & Garden', emoji: '🏠' },
'4': { name: 'Sports', emoji: '🎾' }
};

const category = categories[input];
if (category) {
// In production, fetch from database
await sendProductList(from, category);
state.stage = 'main';
return '\nType "menu" to return to main menu';
}

return MENU.products.text;
}

// Handle order tracking
async function handleOrderTracking(from, input, state) {
// Validate order number format
if (!/^ORD-\d{5}$/.test(input.toUpperCase())) {
return 'Invalid order number. Please use format: ORD-12345';
}

// In production, fetch from database
const orderNumber = input.toUpperCase();
const orderStatus = await getOrderStatus(orderNumber);

state.stage = 'main';

if (orderStatus) {
return `📦 Order Status for ${orderNumber}

Status: ${orderStatus.status}
Location: ${orderStatus.location}
Estimated Delivery: ${orderStatus.estimatedDelivery}

Track: ${orderStatus.trackingUrl}`;
} else {
return `❌ Order ${orderNumber} not found. Please check your order number.`;
}
}

// Handle support inquiries
async function handleSupport(from, input, state) {
// Log support ticket (save to database)
const ticketId = await createSupportTicket(from, input);

state.stage = 'main';

return `✅ Support ticket created: #${ticketId}

Our team will respond within 2 hours.

You'll receive updates via WhatsApp.

Type "menu" to return to main menu`;
}

// Send WhatsApp message
async function sendWhatsAppMessage(to, text, options = {}) {
try {
const response = await afriroute.whatsapp.send({
to: to,
type: 'text',
text: {
body: text,
preview_url: false
},
...options
});

console.log(`Message sent to ${to}: ${response.messageId}`);
return response;
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}

// Send product list with images
async function sendProductList(to, category) {
// Send product catalog with buttons
await afriroute.whatsapp.send({
to: to,
type: 'interactive',
interactive: {
type: 'list',
header: {
type: 'text',
text: `${category.emoji} ${category.name}`
},
body: {
text: 'Browse our top products:'
},
action: {
button: 'View Products',
sections: [
{
title: 'Featured',
rows: [
{
id: 'prod_1',
title: 'Product 1',
description: '$99.99'
},
{
id: 'prod_2',
title: 'Product 2',
description: '$149.99'
}
]
}
]
}
}
});
}

// Mock function - replace with actual database query
async function getOrderStatus(orderNumber) {
return {
status: 'In Transit',
location: 'Nairobi Distribution Center',
estimatedDelivery: 'Dec 13, 2025',
trackingUrl: 'https://track.afrishop.com/' + orderNumber
};
}

// Mock function - replace with actual database insert
async function createSupportTicket(phone, message) {
const ticketId = 'TKT' + Date.now();
console.log(`Created ticket ${ticketId} for ${phone}: ${message}`);
return ticketId;
}

// Verify webhook signature
function verifyWebhook(signature, payload) {
// Implement HMAC verification
// const expectedSignature = crypto
// .createHmac('sha256', process.env.WEBHOOK_SECRET)
// .update(JSON.stringify(payload))
// .digest('hex');
// return signature === expectedSignature;
return true; // Simplified for tutorial
}

// Webhook verification endpoint (for Meta)
app.get('/webhook/whatsapp', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];

if (mode === 'subscribe' && token === process.env.WEBHOOK_SECRET) {
console.log('Webhook verified!');
res.status(200).send(challenge);
} else {
res.status(403).send('Forbidden');
}
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🤖 WhatsApp chatbot running on port ${PORT}`);
console.log(`📡 Webhook URL: http://localhost:${PORT}/webhook/whatsapp`);
});

Step 3: Test Locally with ngrok​

Start ngrok Tunnel​

# Install ngrok: https://ngrok.com/download
ngrok http 3000

You'll get a URL like: https://abc123.ngrok.io

Configure Webhook in AfriRoute Dashboard​

  1. Go to www.afriroute.ai/auth/login/webhooks
  2. Add new webhook:
    • URL: https://abc123.ngrok.io/webhook/whatsapp
    • Events: message.received, message.delivered, message.read
    • Secret: Your webhook secret from .env

Start Your Bot​

node bot.js

Step 4: Test Your Chatbot​

Send a WhatsApp message to your business number:

You: Hi
Bot: 👋 Welcome to AfriShop!

Please select an option:
1️⃣ View Products
2️⃣ Check Order Status
3️⃣ FAQs
4️⃣ Contact Support
5️⃣ About Us

You: 1
Bot: 📦 Our Products:

1. Electronics 💻
2. Fashion 👗
3. Home & Garden �
4. Sports 🎾

Reply with a number or type "back" for main menu

Step 5: Add AI-Powered Responses (Optional)​

For intelligent, context-aware responses:

const { Configuration, OpenAIApi } = require('openai');

const openai = new OpenAIApi(new Configuration({
apiKey: process.env.OPENAI_API_KEY
}));

async function getAIResponse(userMessage, conversationHistory) {
const response = await openai.createChatCompletion({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful customer service agent for AfriShop, an e-commerce platform in Africa.'
},
...conversationHistory,
{
role: 'user',
content: userMessage
}
],
max_tokens: 150
});

return response.data.choices[0].message.content;
}

Step 6: Deploy to Production​

Using Heroku​

# Install Heroku CLI
heroku login
heroku create afriroute-whatsapp-bot

# Set environment variables
heroku config:set AFRIROUTE_API_KEY=your_key
heroku config:set AFRIROUTE_API_SECRET=your_secret
heroku config:set WEBHOOK_SECRET=your_webhook_secret

# Deploy
git push heroku main

Using AWS Lambda + API Gateway​

// lambda-handler.js
exports.handler = async (event) => {
// Parse webhook
const body = JSON.parse(event.body);

// Process message
await handleIncomingMessage(body.message);

return {
statusCode: 200,
body: JSON.stringify({ success: true })
};
};

Best Practices​

1. Message Rate Limiting​

const rateLimit = new Map();

function isRateLimited(phone) {
const now = Date.now();
const lastMessage = rateLimit.get(phone) || 0;

if (now - lastMessage < 1000) { // 1 message per second
return true;
}

rateLimit.set(phone, now);
return false;
}

2. Persistent Storage​

Use Redis for conversation states:

const redis = require('redis');
const client = redis.createClient();

async function getConversationState(phone) {
const state = await client.get(`conversation:${phone}`);
return state ? JSON.parse(state) : { stage: 'main', data: {} };
}

async function saveConversationState(phone, state) {
await client.set(`conversation:${phone}`, JSON.stringify(state), {
EX: 3600 // Expire after 1 hour
});
}

3. Error Handling​

async function handleIncomingMessage(message) {
try {
// Process message
await processMessage(message);
} catch (error) {
console.error('Error processing message:', error);

// Send fallback message
await sendWhatsAppMessage(
message.from,
'Sorry, something went wrong. Please try again or type "support" for help.'
);
}
}

4. Analytics Tracking​

async function trackEvent(phone, event, data) {
await analytics.track({
userId: phone,
event: event,
properties: data,
timestamp: new Date()
});
}

// Usage
trackEvent(phone, 'menu_selected', { option: '1' });
trackEvent(phone, 'order_tracked', { orderId: 'ORD-12345' });

Advanced Features​

1. Send Rich Media​

// Send image with caption
await afriroute.whatsapp.send({
to: phone,
type: 'image',
image: {
url: 'https://cdn.afrishop.com/products/laptop.jpg',
caption: '💻 MacBook Pro - $1,299\n\nReply "BUY" to purchase'
}
});

// Send document
await afriroute.whatsapp.send({
to: phone,
type: 'document',
document: {
url: 'https://cdn.afrishop.com/invoice.pdf',
filename: 'Invoice_ORD12345.pdf',
caption: 'Your invoice'
}
});

2. Interactive Buttons​

await afriroute.whatsapp.send({
to: phone,
type: 'interactive',
interactive: {
type: 'button',
body: {
text: 'Would you like to proceed with this order?'
},
action: {
buttons: [
{
type: 'reply',
reply: {
id: 'confirm_yes',
title: 'Yes, proceed'
}
},
{
type: 'reply',
reply: {
id: 'confirm_no',
title: 'No, cancel'
}
}
]
}
}
});

3. Location Sharing​

// Send location
await afriroute.whatsapp.send({
to: phone,
type: 'location',
location: {
longitude: 36.8219,
latitude: -1.2921,
name: 'AfriShop Store - Nairobi',
address: '123 Kenyatta Avenue, Nairobi'
}
});

Troubleshooting​

Bot Not Responding​

Check:

  • ✅ Webhook URL is publicly accessible (use ngrok for testing)
  • ✅ Webhook is configured in AfriRoute dashboard
  • ✅ API credentials are correct
  • ✅ Server is running and listening on correct port

Messages Delayed​

Solutions:

  • Use Redis instead of in-memory storage
  • Implement message queues (RabbitMQ, SQS)
  • Scale horizontally with load balancer

Rate Limit Errors​

Fix:

const { promiseRetry } = require('promise-retry');

await promiseRetry(async (retry) => {
try {
return await afriroute.whatsapp.send(message);
} catch (error) {
if (error.code === 'RATE_LIMIT_EXCEEDED') {
retry(error);
}
throw error;
}
}, { retries: 3, minTimeout: 1000 });

Next Steps​

Now that you've built a WhatsApp chatbot, explore:

Resources​

Pro Tip

Store conversation history for context-aware responses and better user experience!