← All posts
Case Study

One LINE Webhook for the Whole Japan Team: How a Fukuoka Startup Routes LINE Messages to Slack, CRM, and AI

At 9:47 on a Tuesday morning, a customer in Osaka sent a LINE message to their account manager: “The CSV export keeps failing — is this a known issue?” The account manager was in a meeting. The message sat on her phone for three hours. By the time she replied, the customer had already filed a support ticket through the website — and tagged the company on X, asking if anyone was actually monitoring their LINE channel.

This is not a story about platform limitations. LINE works fine as a messaging app. The problem was that six people in a Fukuoka office were each receiving customer messages on their own LINE accounts, on their own phones, with no shared view of who was responding to what. The company — a B2B SaaS team that sells a logistics planning tool to warehouses across Japan and Southeast Asia — had grown past the point where “everyone checks their own LINE” was a viable support strategy. But the alternatives all seemed to require migrating customers off LINE, which in Japan is a non-starter.

The Queue That Wasn’t

The team’s support setup in early 2026 looked like this: three sales reps and two support engineers each had a LINE account that customers had added as a friend. Messages arrived on individual phones. There was no shared inbox, no response tracking, no SLA. If someone was out sick, their messages sat unread until they returned. If a customer messaged after 7 PM, they waited until the next business day — not because the team didn’t want to help, but because nobody knew the message had arrived.

The company had considered LINE’s Official Account (OA) path, which offers a shared inbox and some automation. But the OA application process takes up to 60 business days for verification, and the Unverified tier caps contact lists at 500 friends — a hard ceiling for a team whose customer base was approaching 800. The Verified and Premium tiers unlock higher limits but come with monthly fees and interaction-window rules that govern when a business can initiate contact. This team didn’t initiate contact. They responded. The OA broadcast machinery was solving a problem they didn’t have.

Meanwhile, the operational cost of the status quo was compounding. In the first quarter of 2026, the team logged 23 instances where a customer message went unanswered for more than four hours. Seven of those led to escalated support tickets through other channels. Two led to public complaints on X. The support lead, Kenji, started a spreadsheet to track missed responses. He stopped updating it after week six.

One Webhook, Three Destinations

The turning point wasn’t a crisis — it was a Slack message from the company’s CTO, who had been reading about webhook-based message routing. The idea was simple: instead of each person checking their own LINE, connect all six LINE accounts through a single interface that forwards incoming messages to the tools the team already used. Slack for real-time visibility. Their CRM for logging and follow-up. And, for routine questions, an AI draft that the team could review before sending.

The team connected their LINE accounts to UnifyPort using QR code authentication — the only auth mode LINE supports on the platform. Each account scan took about 30 seconds. Within an hour, all six accounts were live, and a single webhook endpoint was receiving every incoming LINE message as a message.received event.

The webhook payload looked like this:

{
  "id": "evt_9a3f7c2e01",
  "type": "message.received",
  "provider": "line",
  "account_id": "acc_4b82d1",
  "occurred_at": "2026-05-12T01:47:23Z",
  "data": {
    "conversation": {
      "id": "U4af2c891...",
      "type": "user",
      "title": "Tanaka-san"
    },
    "sender": {
      "id": "U4af2c891...",
      "type": "user",
      "name": "Tanaka-san"
    },
    "message": {
      "id": "msg_18420...",
      "type": "text",
      "text": "The CSV export keeps failing — is this a known issue?",
      "direction": "inbound",
      "sent_at": "2026-05-12T01:47:22Z"
    },
    "event": {
      "kind": "message_received"
    }
  }
}

Every delivery arrived with an X-Device-Signature header — an HMAC-SHA256 hex digest of the timestamp concatenated with the raw request body, signed with the webhook’s signing_secret. The team’s Express handler verified the signature before processing anything:

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

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

app.post('/webhook', 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');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.type === 'message.received') {
    await routeMessage(event);
  }
  res.status(200).end();
});

The interesting part wasn’t the webhook handler — every integration has one. It was the routeMessage function that ran after verification. Instead of dumping every message into a single queue, the routing logic inspected the message content and the conversation context to decide where it should go.

Routing Logic: Slack, CRM, AI — or All Three

The team’s routing rules evolved over the first two weeks, but the final version had three tiers:

Tier 1 — Slack notification (every message). Every incoming LINE message posted to #support-line in Slack with the sender name, message preview, and the account_id so the team knew which rep’s account received it. This alone solved the “message sat on someone’s phone” problem. If the account owner was in a meeting, anyone else on the team could see the message and respond through UnifyPort’s POST /v1/messages endpoint using the reply_to.reply_token from the original message.

Tier 2 — CRM logging (every message). A parallel call created or updated a contact record in their CRM with the full message text, timestamp, and conversation ID. Before this, customer interactions on LINE were invisible to the CRM — they existed only in individual chat histories on phones. Now every LINE conversation had a complete audit trail alongside email and website ticket history.

Tier 3 — AI draft (routine questions only). A simple keyword classifier checked incoming messages against a list of common topics: CSV export, billing, login issues, API documentation requests. If a match was found, the message was forwarded to an internal LLM endpoint with a system prompt that included the relevant documentation section. The AI’s draft response was posted to a #support-drafts Slack channel with a “Review & Send” button. No response went out without human approval — but the draft was ready in seconds, and the rep only needed to click once.

async function routeMessage(event) {
  const { conversation, sender, message } = event.data;
  const accountId = event.account_id;

  // Tier 1: Always notify Slack
  await postToSlack('#support-line', {
    account: accountId,
    sender: sender.name,
    preview: message.text?.slice(0, 120),
    reply_token: message.reply_token,
  });

  // Tier 2: Always log to CRM
  await crm.upsertContact({
    external_id: sender.id,
    platform: 'line',
    name: sender.name,
    last_message: message.text,
    last_message_at: message.sent_at,
  });

  // Tier 3: AI draft for routine questions
  const topic = classifyTopic(message.text);
  if (topic) {
    const draft = await generateDraft(topic, message.text, sender.name);
    await postToSlack('#support-drafts', {
      sender: sender.name,
      topic,
      draft,
      account_id: accountId,
      reply_token: message.reply_token,
    });
  }
}

The classifyTopic function was deliberately simple — a keyword map, not a neural classifier. “CSV,” “export,” “download” mapped to the export-docs topic. “請求,” “料金,” “支払い” mapped to billing. Keeping it rule-based meant the team could debug routing failures in minutes, not hours.

Conversation Labels for Team Ownership

One feature the team adopted in the second week was UnifyPort’s conversation labels. Using the POST /v1/conversations/labels endpoint, they created labels for each team member: kenji, yuki, hiro, sales. When a message arrived, the routing function tagged the conversation with the label of the account owner. If the account owner was out of office (checked via a simple calendar integration), the conversation was re-labeled to the on-call rep.

This meant that at any point, any team member could call GET /v1/conversations/labels and see exactly which conversations were assigned to them — across all six LINE accounts. The shared inbox problem was solved not by creating a new inbox, but by labeling conversations across existing ones.

The labels also powered a weekly report: a cron job pulled all conversations tagged with each rep’s label, counted response times (measured from message.received occurred_at to the first outbound POST /v1/messages call), and posted a summary to #support-metrics every Monday morning.

Week One vs Week Four

The numbers told the story. In the week before the webhook went live, the team’s median response time on LINE was 3 hours and 40 minutes, with 11 messages going unanswered for more than four hours. In week four, the median response time was 22 minutes. Zero messages went unanswered for more than four hours — the worst case was a 1-hour-15-minute response during a company-wide offsite.

The AI draft tier handled about 35% of incoming messages. The team approved and sent roughly 80% of those drafts with no edits. The remaining 20% needed minor rewrites — usually because the customer’s question had a nuance the keyword classifier missed. Even in those cases, the draft gave the rep a starting point, cutting composition time from minutes to seconds.

The CRM integration had an unexpected side effect: the sales team started using LINE conversation history to prepare for renewal calls. Before the webhook, a rep going into a renewal meeting had no record of the customer’s past support interactions on LINE. Now they could see every message, every response time, and every resolution — all logged automatically.

What the Team Didn’t Change

The customers noticed nothing. They still messaged the same LINE accounts they’d been using for months. They still got replies from the same people. The only difference was that replies came faster, and if their usual rep was unavailable, someone else picked up — because the message was visible to the whole team in Slack, not buried on one person’s phone.

The team also kept their LINE accounts as personal accounts, not OA accounts. The UnifyPort webhook delivers inbound messages regardless of account type — the OA verification queue and interaction windows are outbound-broadcast concerns, and this team’s workflow was entirely inbound. If they ever decide to run proactive campaigns (seasonal promotions, product announcements), the OA path will be relevant. Until then, it’s a layer of bureaucracy they don’t need.

The Pattern

This isn’t a LINE-specific story. Any team that receives customer messages across multiple personal accounts on any platform — WhatsApp, Telegram, X, Zalo — faces the same routing problem. The webhook doesn’t care which platform the message came from; the provider field changes, but the payload shape, the HMAC-SHA256 verification, and the routing logic stay identical. The Fukuoka team is already planning to add their WhatsApp account (used by Southeast Asian warehouse partners) to the same routing system. One handler, one set of labels, one metrics pipeline.

If your team’s support strategy is “everyone checks their own messages,” the question isn’t whether you need a better platform — it’s whether your messages need a better routing layer. A single webhook endpoint, 20 lines of routing logic, and the tools you already use might be enough.