← All posts
Case Study

How a Vietnam Support Team Put Zalo, WhatsApp, and LINE Into One Inbound Queue

At 8:12 on a Monday morning in Ho Chi Minh City, the first Zalo message arrived before the support lead had opened her laptop. A customer wanted to change a delivery address. Four minutes later, a supplier in Bangkok sent a LINE message about a delayed carton. At 8:27, a repeat buyer from Singapore asked about warranty terms over WhatsApp.

None of those messages were unusual. The problem was that they landed in three different places, on three different phones, owned by three different people. By 9:00, the team had already copied two screenshots into Slack, forwarded one message to a sales rep, and forgotten which customer had asked the warranty question.

The company was a five-person operations team running cross-border order support for a small beauty brand. Their customers were mostly in Vietnam, Thailand, and Singapore, so the channel mix was predictable: Zalo for local buyers, LINE for Thai partners, WhatsApp for suppliers and repeat international customers. The business did not need campaigns, broadcast templates, or a new CRM. It needed a reliable way to see inbound messages as work items.

Three Inboxes, One SLA

Before the change, their support process was manual but familiar. Zalo stayed on the Vietnam support lead’s phone. LINE stayed with the operations coordinator who handled Thai suppliers. WhatsApp stayed with the founder because older customers still had his number. Every morning, the team opened Slack and posted anything that looked urgent.

This worked until volume crossed a threshold. A delivery-address change missed before courier pickup became a refund. A supplier’s LINE message about delayed cartons got noticed after the warehouse had promised same-day dispatch. A WhatsApp warranty question sat unread because the founder was traveling.

The team tracked 31 late replies in two weeks. Most were not technical failures. They were visibility failures. Someone had seen the message, but the rest of the team had not. Or the right person was unavailable, and nobody knew the message existed.

The official platform routes each solved a different, larger problem. Zalo Official Account was useful if the brand wanted official account operations and notification templates. LINE Official Account was useful for rich menus, broadcast operations, and branded customer presence. WhatsApp Cloud API was useful for template-based business messaging at scale. But the team was not trying to initiate conversations at scale. Every important conversation started with the customer or supplier.

That difference changed the project. The requirement was not “adopt three official business messaging stacks.” It was “turn every inbound message into one queue item we can trust.”

The Webhook They Actually Needed

The team created one endpoint in their backend:

POST /inbound/messages

Then they connected one Zalo account, one WhatsApp account, and one LINE account through UnifyPort. All three accounts delivered to the same webhook endpoint. Every delivery used the same envelope: id, type, provider, account_id, occurred_at, and data.

A Zalo message looked like this after normalization:

{
  "id": "evt_7f4c20a91d",
  "type": "message.received",
  "provider": "zalo",
  "account_id": "acc_vn_support",
  "occurred_at": "2026-07-02T01:12:16Z",
  "data": {
    "conversation": { "id": "zalo_user_1942", "type": "user", "title": "Minh Anh" },
    "sender": { "id": "zalo_user_1942", "type": "user", "name": "Minh Anh" },
    "message": {
      "id": "zalo_msg_8831",
      "type": "text",
      "text": "Em muốn đổi địa chỉ giao hàng trước 11h được không?",
      "direction": "inbound",
      "sent_at": "2026-07-02T01:12:15Z"
    },
    "event": { "kind": "message_received" }
  }
}

The same handler processed WhatsApp and LINE. The provider changed, but the routing code did not need a different parser for each platform.

Before the handler trusted a payload, it verified the delivery signature. The endpoint had a signing_secret, so each request included X-Device-Timestamp and X-Device-Signature. The signature was the HMAC-SHA256 hex digest of the timestamp, a dot, and the raw request body.

import crypto from 'crypto';
import express from 'express';

const app = express();
const secret = process.env.WEBHOOK_SIGNING_SECRET;

app.post('/inbound/messages', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestamp = req.get('X-Device-Timestamp');
  const signature = req.get('X-Device-Signature');

  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(timestamp + '.');
  hmac.update(req.body);
  const expected = hmac.digest('hex');

  const valid = signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) {
    res.status(401).end();
    return;
  }

  const event = JSON.parse(req.body.toString('utf8'));
  await enqueueInboundMessage(event);
  res.status(200).end();
});

The queue item stored five fields that mattered to the team: platform, customer name, message text, conversation ID, and account ID. Everything else stayed attached as raw event metadata for later audit.

Routing Rules the Team Could Explain

The first version of routing was intentionally boring.

Zalo messages went to the Vietnam support lane. LINE messages went to the supplier lane. WhatsApp messages went to the international customer lane. Any message containing an order number also created a CRM note. Any message containing delivery keywords posted to the #fulfillment Slack channel.

There was no AI classifier in the first week. The team wanted a system they could debug while orders were moving. They used rule names like delivery_change, supplier_delay, and warranty_question, and every routed item showed the rule that matched it.

By the second week, they added one assistive step: draft suggestions for three routine questions. Address changes got a short reply asking for the new address and order number. Warranty questions got a reply template with the warranty window and photo requirements. Supplier delay messages got a response asking for an updated handoff time. The drafts were posted to Slack, not sent automatically.

That distinction mattered. The team did not want automation to talk to customers. It wanted automation to make the next human action obvious.

What Changed After Four Weeks

The visible change was simple: nobody watched three phones anymore. The support lead opened one queue each morning and saw Zalo, WhatsApp, and LINE in the same list. The team could filter by provider, but the default view was time-ordered inbound work.

Median first response time dropped from 2 hours 18 minutes to 26 minutes. Messages older than four hours fell from 31 in two weeks to 3 in the next two weeks. More importantly, the team stopped using screenshots as operational records. Each inbound event had a stable event ID, a provider, an account ID, and a timestamp.

The CRM also became more useful. Before the webhook, a customer’s Zalo history lived on one phone and a supplier’s LINE context lived with one coordinator. After the change, the CRM showed the last message regardless of channel. When the founder prepared for a renewal call, he no longer asked the team to search their chats. He opened the contact record.

The team did not remove Zalo, WhatsApp, or LINE from customer workflows. Customers kept messaging the same accounts. The only thing that changed was the backend path after the message arrived.

The Pattern

This is the pattern that matters for small teams: inbound support is not the same problem as outbound campaign infrastructure. Official business products are valuable when you need verified presence, broadcasts, templates, rich menus, or campaign operations. But if the immediate pain is “customers message us first and the team misses messages,” the first system you need is an inbound routing layer.

With UnifyPort, the team treated every platform as one event stream. message.received became the common contract. HMAC-SHA256 verification made deliveries trustworthy. The provider field told the queue where the message came from, and the rest of the support workflow stayed platform-neutral.

For a five-person team, that was enough. One webhook, one queue, and the tools they already used.