Bonaventure OgetoBy Bonaventure Ogeto|

WhatsApp Bot That Collects Payment via Paystack

A WhatsApp bot that collects payment through Paystack works by receiving customer messages via the WhatsApp Business API, guiding them through product selection and order confirmation, then generating a Paystack payment link and sending it in the chat. When the customer pays, a Paystack webhook triggers a confirmation message back to the customer on WhatsApp.

Architecture Overview

A WhatsApp payment bot has three moving parts that talk to each other through webhooks and API calls:

  1. WhatsApp Business API. This is how your bot sends and receives messages. You register a phone number, configure a webhook URL, and WhatsApp delivers incoming messages to your server as HTTP POST requests. Your server sends replies by calling the WhatsApp API.
  2. Your backend server. This is the brain. It receives incoming WhatsApp messages, manages conversation state, stores orders, generates Paystack payment links, and handles Paystack webhooks. A single Express or Fastify server handles all of this.
  3. Paystack API. You call Transaction Initialize to generate payment links. Paystack sends charge.success webhooks when customers pay. Your server receives these and triggers confirmation messages back through WhatsApp.

The data flow for a complete purchase looks like this:

  1. Customer sends "Hi" on WhatsApp.
  2. WhatsApp API delivers the message to your webhook.
  3. Your server replies with a product menu.
  4. Customer selects a product.
  5. Your server confirms the order and calls Paystack to generate a payment link.
  6. Your server sends the payment link to the customer on WhatsApp.
  7. Customer taps the link and pays on the Paystack checkout page.
  8. Paystack sends a charge.success webhook to your server.
  9. Your server sends a payment confirmation to the customer on WhatsApp.

Two separate webhooks are at play: one from WhatsApp (incoming messages) and one from Paystack (payment events). Your server needs to handle both on different endpoints.

Choosing a WhatsApp API Provider

You cannot send automated WhatsApp messages from a personal account. You need access to the WhatsApp Business API. There are three main ways to get it:

Meta Cloud API (direct). This is Meta's own hosted WhatsApp API. You set it up through the Meta for Developers portal. Free for the first 1,000 conversations per month. No third-party middleman. The documentation is decent but can be confusing for first-timers. This is the option most developers in Kenya should start with because it has the lowest cost.

Africa's Talking. A Nairobi-based communications platform that offers WhatsApp Business API access alongside SMS, USSD, and voice. If you already use Africa's Talking for SMS or USSD, adding WhatsApp is straightforward. Their API wraps Meta's WhatsApp API with a more familiar interface for African developers.

Twilio. A global communications platform with WhatsApp API access. Mature SDK, good documentation, and predictable pricing. More expensive than the direct Meta option, but some teams prefer Twilio's developer experience.

For this guide, the code examples use the Meta Cloud API directly. The patterns are the same regardless of provider. You receive a message webhook, process it, and send a reply.

Whichever provider you choose, you need:

  • A verified Meta Business account.
  • A phone number dedicated to your bot (not your personal number).
  • A webhook endpoint on a publicly accessible server.
  • Approved message templates for any messages sent outside the 24-hour window.

Setting Up the WhatsApp Webhook

When a customer sends a message to your WhatsApp Business number, Meta delivers it to your webhook as an HTTP POST request. You need to set up two things: a GET endpoint for webhook verification and a POST endpoint for receiving messages.

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

// WhatsApp webhook verification (GET)
app.get('/webhooks/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.WA_VERIFY_TOKEN) {
    return res.status(200).send(challenge);
  }

  res.sendStatus(403);
});

// WhatsApp incoming messages (POST)
app.post('/webhooks/whatsapp', async (req, res) => {
  // Always return 200 immediately
  res.status(200).send('OK');

  const body = req.body;

  if (!body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]) {
    return; // Not a message event (could be a status update)
  }

  const message = body.entry[0].changes[0].value.messages[0];
  const customerPhone = message.from; // Format: 254712345678
  const messageText = message.text?.body?.trim().toLowerCase() || '';

  // Route to conversation handler
  await handleConversation(customerPhone, messageText);
});

The GET endpoint is called once when you register the webhook with Meta. The POST endpoint receives every incoming message. Return 200 immediately on the POST, then process the message asynchronously. If you take too long to respond, Meta may retry the delivery, causing duplicate processing.

Conversation Flow: Browse, Select, Pay

A payment bot needs a conversation flow that feels natural, not like filling out a form. The simplest effective flow has four states:

// Conversation states
var STATES = {
  IDLE: 'idle',           // Waiting for the customer to start
  BROWSING: 'browsing',   // Showing products
  CONFIRMING: 'confirming', // Confirming order details
  AWAITING_PAYMENT: 'awaiting_payment', // Payment link sent
};

// Simple in-memory state (use Redis or a database in production)
var conversations = new Map();

async function handleConversation(phone, text) {
  var state = conversations.get(phone) || { step: STATES.IDLE, cart: [] };

  switch (state.step) {
    case STATES.IDLE:
      await sendProductMenu(phone);
      state.step = STATES.BROWSING;
      break;

    case STATES.BROWSING:
      var product = findProduct(text);
      if (product) {
        state.cart.push(product);
        await sendMessage(phone,
          'Added ' + product.name + ' (KES ' + product.price + '). ' +
          'Reply "done" to checkout or pick another item.'
        );
      } else if (text === 'done' && state.cart.length > 0) {
        var total = state.cart.reduce(function(s, p) { return s + p.price; }, 0);
        var summary = state.cart.map(function(p) { return p.name; }).join(', ');
        await sendMessage(phone,
          'Your order: ' + summary + '
Total: KES ' + total.toLocaleString() + '

' +
          'Reply "confirm" to get your payment link or "cancel" to start over.'
        );
        state.step = STATES.CONFIRMING;
      } else {
        await sendMessage(phone, 'I did not recognize that. Reply with a product number or name.');
      }
      break;

    case STATES.CONFIRMING:
      if (text === 'confirm') {
        var total = state.cart.reduce(function(s, p) { return s + p.price; }, 0);
        var paymentLink = await generatePaystackLink(phone, state.cart, total);
        await sendMessage(phone,
          'Pay here: ' + paymentLink + '

Tap the link to pay with M-Pesa or card. ' +
          'I will confirm once your payment goes through.'
        );
        state.step = STATES.AWAITING_PAYMENT;
      } else if (text === 'cancel') {
        state = { step: STATES.IDLE, cart: [] };
        await sendMessage(phone, 'Order cancelled. Send any message to start again.');
      }
      break;

    case STATES.AWAITING_PAYMENT:
      await sendMessage(phone,
        'I sent you a payment link. Please complete the payment there. ' +
        'If the link expired, reply "new link" and I will generate a fresh one.'
      );
      if (text === 'new link') {
        var total = state.cart.reduce(function(s, p) { return s + p.price; }, 0);
        var newLink = await generatePaystackLink(phone, state.cart, total);
        await sendMessage(phone, 'Here is a new link: ' + newLink);
      }
      break;
  }

  conversations.set(phone, state);
}

This is a minimal state machine. In a production bot, you would add:

  • Product quantities. Let the customer say "2" to add two of something, not just one.
  • Cart removal. Let them remove items before checkout.
  • Email collection. Paystack requires an email for transactions. Ask the customer for their email or use a default one tied to their phone number.
  • Timeout handling. If a customer goes silent for hours, reset their state instead of keeping a stale cart in memory.
  • Persistent state. Use Redis or a database instead of an in-memory Map. If your server restarts, the Map is gone.

Webhook Confirmation Back to the Bot

When the customer completes payment, Paystack sends a charge.success webhook to your server. Your server then sends a confirmation message back to the customer on WhatsApp.

// Paystack webhook handler
app.post('/webhooks/paystack', express.json(), async (req, res) => {
  res.status(200).send('OK');

  var hash = crypto
    .createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (hash !== req.headers['x-paystack-signature']) {
    console.error('Invalid Paystack webhook signature');
    return;
  }

  var event = req.body;

  if (event.event === 'charge.success') {
    var reference = event.data.reference;
    var amount = event.data.amount;
    var metadata = event.data.metadata;
    var phone = metadata.customer_phone;
    var orderId = metadata.order_id;

    // Idempotency check: only process each payment once
    var order = await getOrder(orderId);
    if (!order || order.status === 'paid') {
      return; // Already processed or unknown order
    }

    // Verify amount matches
    if (amount !== order.totalKES * 100) {
      console.error('Amount mismatch for ' + orderId +
        ': expected ' + (order.totalKES * 100) + ', got ' + amount);
      return;
    }

    // Update order status
    await updateOrderStatus(orderId, 'paid', { paystackRef: reference });

    // Update conversation state
    conversations.set(phone, { step: STATES.IDLE, cart: [] });

    // Send confirmation on WhatsApp
    var items = order.items.map(function(p) { return p.name; }).join(', ');
    await sendMessage(phone,
      'Payment received! Your order (' + items + ') is confirmed. ' +
      'We will send you a delivery update shortly. Thank you!'
    );
  }
});

The idempotency check is essential. Paystack may retry the webhook if your server was slow to respond. Without the check, the customer gets duplicate "Payment received!" messages. Check the order status before processing. If it is already "paid," skip the message.

Also verify the amount. If the webhook says the customer paid KES 500 but the order was KES 5,000, something is wrong. Do not confirm the order.

Order Status Updates via WhatsApp

After payment confirmation, the customer expects updates. Here is where the 24-hour messaging window becomes important.

Within 24 hours of the customer's last message, you can send freeform text messages. After 24 hours, you need pre-approved message templates. For a payment bot, you should create templates for:

  • Order confirmation. "Your order {{order_id}} has been confirmed. Total: KES {{amount}}."
  • Dispatch notification. "Your order {{order_id}} is on the way. Estimated delivery: {{eta}}."
  • Delivery confirmation. "Your order {{order_id}} has been delivered. Thank you for your purchase."

Submit these templates to Meta for approval before you need them. Approval takes anywhere from a few minutes to a couple of days.

// Send a template message (for messages outside the 24-hour window)
async function sendTemplateMessage(phone, templateName, parameters) {
  var url = 'https://graph.facebook.com/v18.0/' +
    process.env.WHATSAPP_PHONE_ID + '/messages';

  await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.WHATSAPP_TOKEN,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      to: phone,
      type: 'template',
      template: {
        name: templateName,
        language: { code: 'en' },
        components: [
          {
            type: 'body',
            parameters: parameters.map(function(p) {
              return { type: 'text', text: p };
            }),
          },
        ],
      },
    }),
  });
}

// Usage:
// await sendTemplateMessage('254712345678', 'order_dispatched', [
//   'WA-2547-1689900000',  // order_id
//   '45 minutes',          // eta
// ]);

Production Considerations

A working demo and a production bot are different things. Here is what separates them.

Conversation state persistence. Use Redis or a database for conversation state. An in-memory Map works for testing but loses all state when the server restarts. A customer who was mid-order will get confused when the bot forgets them.

Message queue. Do not call the WhatsApp API synchronously from inside your webhook handler. Put outgoing messages in a queue (BullMQ, RabbitMQ, or even a simple database queue) and process them in a worker. This prevents webhook timeouts and handles rate limiting from Meta's API.

Error messages. When something goes wrong (Paystack API down, invalid product, network error), the bot should send a helpful message, not go silent. "Something went wrong. Please try again in a few minutes" is better than nothing.

Logging. Log every incoming message, every outgoing message, every Paystack API call, and every webhook. When a customer says "I paid but did not get confirmation," you need to trace the entire flow to find where it broke.

Rate limiting. Meta's WhatsApp API has rate limits. If your bot suddenly gets popular, you may hit them. Implement backoff and retry logic on your outgoing message calls.

Testing. WhatsApp's test environment lets you send messages without using real phone numbers. Use it during development. For Paystack, use test keys and test cards. Do not develop against production APIs.

Security. Your webhook endpoints are public URLs. Verify the Paystack signature on every webhook. For WhatsApp webhooks, verify the payload against the app secret. Do not trust unverified requests.

Where to Go From Here

This guide covers the core pattern: receive a message, manage a conversation, generate a payment link, and confirm payment. From here, you can extend in several directions.

For a simpler approach without a full bot, see WhatsApp Commerce Plus Paystack Payment Links. That covers manual and semi-automated link sharing without the conversation flow.

For reaching customers who do not use WhatsApp (feature phone users), see USSD Plus Paystack: Reaching Feature Phone Users.

For the full Kenya payments context, including M-Pesa specifics, settlement, and regulatory considerations, start with Paystack in Kenya: M-Pesa, Pesalink, and the Daraja Question.

If you want to build production-grade payment systems and ship real products, the McTaba 26-week Full-Stack Software and AI Engineering bootcamp covers payment integration, API design, and deployment for the African market.

Key Takeaways

  • A WhatsApp payment bot connects three APIs: the WhatsApp Business API for messaging, the Paystack API for payment link generation, and your own backend for order management.
  • The conversation flow follows a predictable pattern: greet, show products, confirm order, send payment link, confirm payment, update delivery status.
  • You can use Meta Cloud API, Africa's Talking, or Twilio for WhatsApp Business API access. Each has different pricing, setup complexity, and phone number requirements.
  • Payment collection uses Paystack payment links, not in-chat payment. The customer taps a link, pays on the Paystack checkout page, and you confirm via webhook.
  • Idempotent webhook handling is critical. The bot must not send duplicate confirmation messages or create duplicate orders when Paystack retries a webhook.
  • The 24-hour WhatsApp messaging window means your bot can only send freeform messages within 24 hours of the customer's last message. After that, you need pre-approved message templates.

Frequently Asked Questions

Do I need a separate phone number for the WhatsApp bot?
Yes. The WhatsApp Business API requires a dedicated phone number that is not registered with WhatsApp on any personal device. You can get a virtual number or a SIM specifically for this purpose. Once registered with the Business API, that number can only be used by the bot, not by a personal WhatsApp account.
Can the bot accept M-Pesa payments directly inside WhatsApp?
No. WhatsApp does not have built-in M-Pesa payment processing in Kenya. The bot generates a Paystack payment link that the customer taps. The payment happens on the Paystack checkout page, which supports M-Pesa (STK push), cards, and other methods. The customer does not need to leave WhatsApp to tap the link, but the checkout opens in their browser.
How much does the WhatsApp Business API cost?
Meta Cloud API gives you 1,000 free service conversations per month. Beyond that, you pay per conversation (not per message). Africa's Talking and Twilio have their own pricing on top of Meta's conversation fees. Check each provider's current pricing page for exact figures.
What happens if the customer closes WhatsApp before completing payment?
The payment link remains valid for a limited time. The customer can come back and tap it later. If they return to the chat and the link has expired, the bot should detect the "awaiting payment" state and offer to generate a new link.
Can I use this approach for a WhatsApp bot on the regular WhatsApp Business app, not the API?
The regular WhatsApp Business app does not support automated messaging or webhooks. You can manually send Paystack payment links in conversations, but you cannot automate the flow. For automation, you need the WhatsApp Business API through Meta Cloud API, Africa's Talking, or Twilio.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon