UnifyPortのインバウンド連携はWebhookから始める:実装チェックリスト
LINEやWhatsAppの問い合わせをシステムに取り込むなら、最初に作るべきものはアカウント接続ではなくWebhook受信口です。UnifyPortには汎用のRESTメッセージ履歴読み取りAPIや、取り逃したpayloadの保証付き再送はありません。したがって、POST /v1/webhook-endpointsで署名付きWebhookを登録し、message.receivedまたは[*]を購読し、イベントを保存してからCRM、AI、有人サポートへ流します。
要点
- 本番のmessaging accountを接続する前にWebhookを登録する。
signing_secretを設定し、X-Device-TimestampとX-Device-Signatureを検証する。- 遅いCRM書き込みやAI処理の前に、標準イベントenvelopeを永続化する。
- 受信専用のinboxなら
message.receivedを購読し、data.message.direction === "inbound"を確認する。 - フィルタ、署名検証、リトライ、業務ルーティングを同じ関数に詰め込まない。
なぜWebhookが先なのか
LINEの友だちからの問い合わせやWhatsAppの注文質問は、CRMやAI agentの準備が終わる前に届くことがあります。受信口が未登録なら、あとから必ず履歴APIで回収できるとは考えないほうが安全です。UnifyPortのQuickstartも、アカウント認証より先にWebhook登録を置いています。
既存記事では、Webhook HMAC replay protectionがタイムスタンプ、署名、冪等性を扱い、UnifyPort webhook event filtersがsubscribed_eventsとワイルドカードの選び方を説明しています。この記事では、その2つを初回実装の順番に落とし込みます。
1. 署名付きendpointを作成する
実際のAPIルートはPOST /v1/webhook-endpointsです。受信inboxだけならmessage.receivedから始めます。すべての公開標準イベントを集めるcollectorなら[*]を使います。
curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
-H "X-Api-Key: $UNIFYPORT_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"url\": \"$PUBLIC_WEBHOOK_URL\",
\"status\": \"active\",
\"subscribed_events\": [\"message.received\"],
\"signing_secret\": \"$WEBHOOK_SIGNING_SECRET\",
\"retry_policy\": { \"max_attempts\": 3 }
}"
retry_policy.max_attemptsは初回配信後のリトライ回数です。ドキュメント上のデフォルトは3、許容範囲は0から5です。実装時はCreate webhook endpointを確認してください。
2. raw bodyを検証する
署名を有効にすると、UnifyPortはX-Device-Signatureを送ります。これは次の文字列に対するHMAC-SHA256のhex digestです。
<X-Device-Timestamp>.<raw request body>
JSON parseや再シリアライズの前に、raw bytesを検証します。Node.jsのcrypto.createHmac()とcrypto.timingSafeEqual()はこの用途に合います。公式ドキュメントでは、timingSafeEqual()に渡すbufferは同じ長さである必要があると説明されています。
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
app.post('/webhooks/unifyport', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.get('X-Device-Timestamp') ?? '';
const signature = req.get('X-Device-Signature') ?? '';
const expected = crypto.createHmac('sha256', signingSecret)
.update(timestamp + '.')
.update(req.body)
.digest('hex');
const valid = /^[0-9a-f]{64}$/i.test(signature) &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
await inbox.insertIfAbsent(event.id, event);
return res.sendStatus(202);
});
本番では、タイムスタンプの新鮮さ、イベントIDによる永続的な重複排除、リトライを前提にしたacknowledgementも追加します。詳細はWebhook delivery and signature verificationを参照してください。
3. 標準イベントenvelopeを保存する
message.receivedのトップレベル形式はproviderをまたいで同じです。日本ではLINEが中心でも、同じ受信口にWhatsAppやTelegramを追加できます。
{
"id": "evt_2f9c1a4b7e",
"type": "message.received",
"provider": "line",
"account_id": "acc_8c21d0",
"occurred_at": "2026-06-08T12:34:56Z",
"data": {
"conversation": { "id": "U4af4980629", "type": "user", "title": "Jordan Lee" },
"sender": { "id": "U4af4980629", "name": "Jordan Lee", "type": "user" },
"message": {
"id": "msg_10472",
"text": "注文は発送済みですか?",
"direction": "inbound",
"sent_at": "2026-06-08T12:34:55Z"
}
}
}
少なくともid、type、provider、account_id、occurred_at、data.conversation.id、data.sender.id、data.message.idを保存します。n8nに渡す場合は、n8n WhatsApp AI agent tutorialのように、検証済みイベントをworkflowに渡す構成にします。
制限と判断基準
UnifyPortのunofficial interfaceは、通常アカウントや既存アカウントから受信イベントを扱いたい小規模チームに向いています。一方、公式認定、公式ビジネス機能、provider固有のポリシー保証が必要なら、各プラットフォームの公式APIを選ぶべきです。またmessage.receivedは受信だけでなく送信側の観測も表せるため、inboxでは必ずdata.message.directionを確認します。
FAQ
最初にどのイベントを購読すべきですか?
受信inboxならmessage.receivedです。全イベントを保存するcollectorでなければ[*]は不要です。
HMACだけで重複処理を防げますか?
いいえ。HMACは完全性と共有secretを確認します。タイムスタンプの新鮮さとイベントIDによる重複排除を追加してください。
CRM処理が終わる前に2xxを返してよいですか?
イベントを自分の永続queueまたはinboxに保存した後なら可能です。CRM、AI、通知は非同期workerに任せます。
次のステップ
Create webhook endpointでreceiverを登録し、webhook delivery guideで署名検証とacknowledgementを固めてから、本番のmessaging accountを接続してください。
Sources
公式情報の確認日:2026-08-26。
メッセージ連携を安定したプロダクトパイプラインへ。
まずは 1 つの API で送信を始め、標準イベントですべての inbound メッセージを業務システムへ戻しましょう。