Tutorial 14 min read

Building a WhatsApp Business Bot — Step by Step Guide

Mahmoud Hamdy
March 22, 2026

WhatsApp is not just popular in the MENA region — it is the default communication layer. Over 90% of smartphone users in Saudi Arabia, Egypt, and the UAE have WhatsApp installed and check it multiple times daily. For businesses, this means your customers are already on the platform, waiting. Building a WhatsApp bot is not about getting them to adopt a new channel; it is about showing up where they already live. This guide walks through building a production-ready WhatsApp Business bot with Node.js and the official WhatsApp Cloud API.

Business API vs. WhatsApp Business App

There are two ways to use WhatsApp for business. The WhatsApp Business App is a free mobile app that works great for one-person operations — you handle messages manually, set up a business profile, and use canned replies. It does not scale past one device and one human.

The WhatsApp Business Platform (formerly the Business API) is the developer-facing product. It supports webhooks, programmatic message sending, message templates, interactive buttons, product catalogs, and concurrent connections. This is what you use to build bots. Since 2022, Meta has offered the Cloud API — a hosted version that removes the need to run your own WhatsApp Business API server. The Cloud API is what I recommend for new projects.

Getting API Access

Access is through the Meta Business Manager. You need: a verified Facebook Business account, a dedicated phone number (cannot be registered to any WhatsApp account already), and a Meta Developer App. The verification process has become faster — in my experience, most businesses get approved within 2–5 business days. Here is the sequence:

  • Create a Meta Developer account at developers.facebook.com
  • Create a Business App and add the WhatsApp product
  • Add your phone number and verify it via SMS or voice call
  • Submit your business for Meta verification (requires business documents)
  • Generate a permanent access token and note your Phone Number ID and Business Account ID

Architecture Overview

The Cloud API architecture is webhook-based. Meta pushes incoming messages to your server; you process them and call the API to send replies. Here is the minimal stack I use:

whatsapp-bot/
├── src/
│   ├── webhook/
│   │   ├── verify.ts        # GET /webhook — Meta verification challenge
│   │   └── handler.ts       # POST /webhook — incoming messages
│   ├── whatsapp/
│   │   ├── client.ts        # API calls (send messages, templates)
│   │   ├── session.ts       # User session state management
│   │   └── types.ts         # WhatsApp message types
│   ├── flows/
│   │   ├── order-status.ts  # Order tracking conversation flow
│   │   ├── catalog.ts       # Product catalog browsing
│   │   └── support.ts       # Human escalation logic
│   └── index.ts             # Express app
├── .env
└── package.json

Setting Up the Webhook

The webhook has two functions: Meta sends a GET request to verify your endpoint when you first configure it, and then sends POST requests for every incoming event. Both must be handled.

// src/webhook/verify.ts
import { Request, Response } from 'express';

export function verifyWebhook(req: Request, res: Response) {
  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_VERIFY_TOKEN) {
    console.log('Webhook verified');
    res.status(200).send(challenge);
  } else {
    res.status(403).send('Forbidden');
  }
}

// src/webhook/handler.ts
export async function handleWebhook(req: Request, res: Response) {
  // Always respond 200 immediately
  res.status(200).send('EVENT_RECEIVED');

  const body = req.body;
  if (body.object !== 'whatsapp_business_account') return;

  for (const entry of body.entry ?? []) {
    for (const change of entry.changes ?? []) {
      const value = change.value;
      if (value.messages) {
        for (const message of value.messages) {
          await processMessage(message, value.metadata.phone_number_id);
        }
      }
    }
  }
}

Sending Messages

All outbound messages go through the same Cloud API endpoint. The key is understanding the message types and when to use each: text for simple replies, template messages for initiating conversations (required outside 24h window), interactive messages for menus and confirmations.

// src/whatsapp/client.ts
import axios from 'axios';

const BASE = `https://graph.facebook.com/v19.0`;

export class WhatsAppClient {
  private phoneNumberId: string;
  private token: string;

  constructor() {
    this.phoneNumberId = process.env.WHATSAPP_PHONE_NUMBER_ID!;
    this.token = process.env.WHATSAPP_ACCESS_TOKEN!;
  }

  async sendText(to: string, body: string): Promise {
    await axios.post(
      `${BASE}/${this.phoneNumberId}/messages`,
      {
        messaging_product: 'whatsapp',
        to,
        type: 'text',
        text: { body, preview_url: false },
      },
      { headers: { Authorization: `Bearer ${this.token}` } }
    );
  }

  async sendInteractiveList(
    to: string,
    bodyText: string,
    buttonText: string,
    sections: WhatsAppListSection[]
  ): Promise {
    await axios.post(
      `${BASE}/${this.phoneNumberId}/messages`,
      {
        messaging_product: 'whatsapp',
        to,
        type: 'interactive',
        interactive: {
          type: 'list',
          body: { text: bodyText },
          action: { button: buttonText, sections },
        },
      },
      { headers: { Authorization: `Bearer ${this.token}` } }
    );
  }

  async sendTemplate(
    to: string,
    templateName: string,
    languageCode: string,
    components: object[]
  ): Promise {
    await axios.post(
      `${BASE}/${this.phoneNumberId}/messages`,
      {
        messaging_product: 'whatsapp',
        to,
        type: 'template',
        template: {
          name: templateName,
          language: { code: languageCode },
          components,
        },
      },
      { headers: { Authorization: `Bearer ${this.token}` } }
    );
  }
}

Message Templates

Templates are pre-approved message formats that you must use when messaging a user who has not contacted you in the last 24 hours. Think of them as transactional messages: order confirmations, shipping notifications, appointment reminders. You submit templates through the Meta Business Manager for review — approval typically takes a few hours to a day.

// Example: sending an order confirmation template
const client = new WhatsAppClient();

await client.sendTemplate(
  '+966501234567',
  'order_confirmation',   // template name you registered
  'ar',                   // language code
  [
    {
      type: 'header',
      parameters: [{ type: 'text', text: 'ORD-4521' }],
    },
    {
      type: 'body',
      parameters: [
        { type: 'text', text: 'Ahmad Al-Rashidi' },
        { type: 'text', text: 'SAR 349.00' },
        { type: 'text', text: 'March 28, 2026' },
      ],
    },
  ]
);

Session Management

WhatsApp does not maintain conversation state for you. Each incoming message is a standalone event. You need to track where each user is in a conversation flow yourself. I use an in-memory Map with a Redis fallback for production:

// src/whatsapp/session.ts
interface UserSession {
  state: 'idle' | 'awaiting_order_id' | 'browsing_catalog' | 'support_escalated';
  data: Record;
  lastActivity: number;
}

const sessions = new Map();

export function getSession(phone: string): UserSession {
  const existing = sessions.get(phone);
  if (existing) return existing;
  const fresh: UserSession = { state: 'idle', data: {}, lastActivity: Date.now() };
  sessions.set(phone, fresh);
  return fresh;
}

export function updateSession(phone: string, updates: Partial): void {
  const session = getSession(phone);
  sessions.set(phone, { ...session, ...updates, lastActivity: Date.now() });
}

// Clean up idle sessions every hour
setInterval(() => {
  const cutoff = Date.now() - 24 * 3_600_000;
  for (const [phone, session] of sessions.entries()) {
    if (session.lastActivity < cutoff) sessions.delete(phone);
  }
}, 3_600_000);

Building the Order Tracking Flow

Here is a full conversation flow that handles order tracking. The user sends any message, gets a menu, selects "Track Order", is prompted for their order number, and gets back the status. The session state machine drives the logic:

// src/flows/order-status.ts
import { getSession, updateSession } from '../whatsapp/session';
import { WhatsAppClient } from '../whatsapp/client';
import { SallaClient } from '../salla/client';

const wa = new WhatsAppClient();

export async function processMessage(message: WAMessage, phoneNumberId: string) {
  const from = message.from;
  const session = getSession(from);

  if (session.state === 'awaiting_order_id') {
    const orderId = message.text?.body?.trim();
    if (!orderId) {
      await wa.sendText(from, 'Please enter a valid order number.');
      return;
    }
    try {
      const salla = new SallaClient(process.env.SALLA_TOKEN!);
      const order = await salla.getOrderByReference(orderId);
      await wa.sendText(from,
        `Order #${order.reference_id}\n` +
        `Status: ${order.status.name}\n` +
        `Total: ${order.amounts.total.amount} SAR`
      );
    } catch {
      await wa.sendText(from, 'Order not found. Please check the number and try again.');
    }
    updateSession(from, { state: 'idle' });
    return;
  }

  // Default: show main menu
  await wa.sendInteractiveList(from, 'How can I help you?', 'Choose an option', [
    {
      title: 'Orders',
      rows: [
        { id: 'track_order', title: 'Track my order', description: 'Check your order status' },
        { id: 'cancel_order', title: 'Cancel an order', description: 'Request order cancellation' },
      ],
    },
    {
      title: 'Support',
      rows: [
        { id: 'talk_to_human', title: 'Talk to a person', description: 'Connect with our team' },
      ],
    },
  ]);
}

Product Catalog Bot

Meta's WhatsApp catalog feature lets you link your product catalog to your Business account and send product messages directly in chat. Customers can browse and add items without leaving WhatsApp. Setting this up requires uploading your catalog through the Commerce Manager, but once done, the API lets you send catalog messages programmatically.

Best Practices and Cost

WhatsApp Cloud API pricing (as of 2026) is conversation-based: each 24-hour conversation window costs a flat rate regardless of message count. Business-initiated conversations (using templates) are more expensive than user-initiated ones. In Saudi Arabia and Egypt, business-initiated conversations cost roughly $0.025–0.040 USD each. For a store handling 500 conversations per month, the cost is around $15–20 — negligible compared to the value.

Key best practices: always respond within 24 hours to keep conversations in the cheaper user-initiated window, use templates sparingly and only for genuine transactional messages, implement rate limiting on your end to avoid API abuse, and always offer a "talk to a human" escape hatch — some customers will never trust a bot for important issues.

Deploying to Production

The webhook endpoint must be publicly accessible over HTTPS. I deploy to a DigitalOcean droplet behind Nginx, with a Let's Encrypt certificate. PM2 keeps the Node.js process alive and restarts it on crashes. The full deployment takes about 20 minutes on a fresh server. One important note: your WEBHOOK_VERIFY_TOKEN should be a long random string — this is what stops other parties from sending fake webhook events to your endpoint.