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
- Go to www.afriroute.ai/auth/login/webhooks
- Add new webhook:
- URL:
https://abc123.ngrok.io/webhook/whatsapp - Events:
message.received,message.delivered,message.read - Secret: Your webhook secret from
.env
- URL:
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!