DEV Community

Cover image for Building a Chat-Based Sales Bot That Doesn't Drop Messages During Flash Sales
Parvej Shah
Parvej Shah

Posted on Originally published at parvejshah.com

Building a Chat-Based Sales Bot That Doesn't Drop Messages During Flash Sales

Originally published at parvejshah.com/blog/conversational-commerce-webhook-architecture by Parvej Shah.

In Bangladesh and much of South and Southeast Asia, e-commerce doesn't look like what a Silicon Valley product manager pictures. Buyers don't browse product catalogs, add items to carts, and check out with saved payment methods. They send a message on Facebook or WhatsApp, ask if an item is in stock, negotiate slightly, confirm their address, and pay by mobile banking transfer. The entire purchase funnel is a conversation.

SellerVai is a platform built for exactly this reality: a 24/7 automated sales assistant that handles order inquiries, processes orders in Bengali and Banglish, and filters fake Cash-on-Delivery (COD) requests across WhatsApp Business API, Facebook Messenger, and Telegram.

The core engineering challenge wasn't the AI. It was the plumbing.

Why Webhooks Are Harder Than They Look

Every message sent to a business on WhatsApp or Facebook triggers an HTTP POST from Meta's servers to your registered webhook URL. The contract is simple: respond with 200 OK within a few seconds, or Meta assumes delivery failed and retries.

When your webhook handler needs to classify intent, query a product database, check inventory, generate a personalized response, and sometimes initiate a payment collection flow — none of which can happen in a few seconds — you have a problem. The naive solution of doing all that work synchronously inside the webhook handler means you're constantly racing against the timeout, and you lose that race regularly during any period of elevated load.

The retry behavior makes it worse. When Meta doesn't get its 200 OK, it retries the same message. Now you have the same message being processed twice, potentially resulting in the same customer getting two replies, the same order being created twice, or two inventory decrements for a single purchase.

The Ingestion Architecture

The solution is to treat the webhook endpoint as nothing more than an authenticated message receiver. Its only responsibility is to verify the signature and acknowledge delivery. All actual processing happens asynchronously.

// Webhook ingestion handler — responds in < 15ms
export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-hub-signature-256") ?? "";

  if (!verifyMetaSignature(rawBody, signature, META_APP_SECRET)) {
    return new Response("Forbidden", { status: 403 });
  }

  const payload = JSON.parse(rawBody) as MetaWebhookPayload;

  await messageQueue.add("process-incoming", {
    channel: "whatsapp",
    rawPayload: payload,
    receivedAt: Date.now(),
  });

  return new Response("OK", { status: 200 });
}
Enter fullscreen mode Exit fullscreen mode

The queue (BullMQ backed by Redis) holds the message until a worker picks it up. The webhook handler has already returned 200 OK to Meta and is completely done. The actual work — intent classification, inventory lookup, response generation — happens in worker processes with no timeout pressure.

The Deduplication Layer

Workers can't blindly process everything in the queue. If Meta retried a message three times before getting its 200 OK, there are three copies of that message in the queue.

Every message gets fingerprinted before processing. The fingerprint is derived from the channel, the sender ID, and the platform's native message ID. The fingerprint goes into Redis with a 5-minute TTL using a SET NX operation — set only if not exists. If the key already exists, that message has been processed recently and the worker skips it.

function buildMessageFingerprint(
  channel: "whatsapp" | "messenger" | "telegram",
  senderId: string,
  messageId: string
): string {
  return crypto
    .createHash("sha256")
    .update(`${channel}:${senderId}:${messageId}`)
    .digest("hex");
}

async function processMessage(event: IncomingMessageEvent): Promise<void> {
  const fingerprint = buildMessageFingerprint(
    event.channel,
    event.senderId,
    event.messageId
  );

  const acquired = await redis.set(
    `processed:${fingerprint}`,
    "1",
    "NX",
    "EX",
    300
  );

  if (!acquired) return; // Duplicate — already processed or in progress

  await runConversationTurn(event);
}
Enter fullscreen mode Exit fullscreen mode

Parsing Bengali and Banglish

Customer messages in social commerce are colloquial and informal. A real message looks like:

"vai ei sneaker ta ki size 42 ache? cash on delivery hobe? dhaka te delivery koto din lagbe?"

Translation: "bro is this sneaker available in size 42? can I pay cash on delivery? how many days will delivery take to Dhaka?"

There are three distinct questions packed into one casual message, written in a mix of Bengali script words and Bengali-language words written in Roman characters.

We use a two-tier parsing approach. A fast regex and keyword engine handles structured data extraction: phone numbers, size numbers, city names, specific product codes. This runs in under 2ms. An LLM classifier handles intent categorization where casual phrasing and code-switching require genuine language understanding.

Flash Sale Traffic

The real stress test came during a promotional campaign. Traffic spiked to roughly 15 times the baseline over a two-hour window. Because the ingestion layer is stateless and the queue absorbs the burst, the webhook endpoints stayed responsive. Workers processed the queue backlog over the following 20 minutes. Every message was processed. No duplicates were sent.

The architecture didn't require any changes for this scenario because it was designed with this scenario in mind from the start. Most reliability problems in messaging systems aren't hard to solve — they just require thinking through the failure modes before you're in them.


Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at parvejshah.com.

Top comments (0)