Introduction
Today we implemented the core Messaging Processing infrastructure for our Node.js chatbot. Leveraging the WhatsApp Webhook model, we came up with a receiver for incoming callbacks, deduplicates messages, marks them as read, and echoes a response—triggering WhatsApp’s “double‑check” (✓✓) receipt experience for the user.
Collaborators
- Roger Soares
- Marcio Galli
Complication
WhatsApp can batch multiple messages (and retries) into a single webhook invocation. Without proper handling:
- Duplicates: Retries or overlapping deliveries can cause the same message to be processed more than once, leading to repeated replies.
- Concurrency: Parallel processing of messages may race against shared state (e.g., marking messages read or persisting conversation context).
- User experience: Extra or missing acknowledgments confuse end users and break the expected flow of check‑marks (from single to double ticks).
To address these challenges, we must support ordered processing pipeline that safely handles multiple incoming messages per webhook, ensuring each is processed exactly once and in a predictable sequence.
WhatsApp Webhook Flow
Authentication (GET)
- WhatsApp sends
hub.mode,hub.verify_tokenandhub.challenge. - We verify the token and return the challenge on success.
- WhatsApp sends
Incoming Messages (POST)
- WhatsApp posts JSON to our
/whatsapp-webhookendpoint. - We immediately acknowledge with
200 EVENT_RECEIVEDto prevent retries.
- WhatsApp posts JSON to our
Message Processing Infrastructure
- Express handles JSON parsing and routing.
- Deduplication: We record each
messageIdin MongoDB’sProcessedMessages. - Read Receipts: Call
markMessageAsRead()so user sees ✓✓. - Handler: A lightweight
defaultHandlerechoes “OK,” completing the acknowledgment cycle.
for each entry → for each change → for each message:
if not in ProcessedMessages:
insert message.id
markMessageAsRead(...)
defaultHandler(...)Deduplication with ProcessedMessages
- Check:
findOne({ messageId })prevents re‑processing duplicates. - Insert: New
messageId+ timestamp marks it as handled. - Outcome: Exactly‑once processing even under retries or batch deliveries.
Read Receipts
After dedupe, we invoke:
await markMessageAsRead(phone_number_id, message.id)User sees two gray ticks turn blue as soon as our reply is accepted by WhatsApp.
Default Handler & Echo Response
Current implementation simply sends back “OK”:
await sendMessage(pnId, from, 'OK')This minimal echo confirms our pipeline works end‑to‑end before adding real business logic.
Example


