製品ガイド
受信メッセージのWebhook(リスナー)
リスナーは、WhatsApp番号の1つをUniboxに接続し、受信メッセージのWebhookをお使いのサーバーに送信できます。ダッシュボードでは、リスナーは 着信トリガー ページで管理します。番号をリスナーにすると、そのチャットがUnibox受信トレイに同期され、Webhook URLを追加した場合は、Wbiztoolが新しいメッセージを1件ずつお使いのサーバーに送信します。Webhookは、CRMへの会話の記録、チームへの通知、自動返信の構築などに利用できます。
始める前に#
- Uniboxアドオン。 料金はWhatsApp番号1つあたり月額$20または年額$200で、作成できるリスナーの数はアドオンの数量と同じです。ダッシュボード内の請求・プランの 利用可能なアドオン から購入してください。アドオンがなくても 着信トリガー ページは開きますが、Add New Listener(新しいリスナーを追加)をクリックすると Unibox Add-on Required(Uniboxアドオンが必要です)と表示され、Subscribe to Unibox Add-on(Uniboxアドオンに登録)ボタンが表示されます。
- WhatsApp設定で 接続済みのWhatsApp番号。追加できるのは、接続済みでまだリスナーになっていない番号のみです。
- ワークスペースのオーナーまたは編集者である必要があります。
- Webhookを使う場合:JSONボディの
POSTリクエストを受け付ける、100文字以内 の公開URL(httpsを使用してください)。
リスナーを追加する#
着信トリガーを開く
サイドバーで Unibox を開いて 着信トリガー をクリックするか、着信トリガーにアクセスします。
新しいリスナーを開始する
新しいリスナーを追加 カードをクリックします。
番号を選ぶ
WhatsApp番号を選択 で番号を選びます。一覧に No available WhatsApp numbers(利用可能なWhatsApp番号がありません)と表示される場合は、接続済みの番号がすべてリスナーになっているか、接続済みの番号がありません。
Webhookを追加する(任意)
Webhook URL(任意) を入力します。URLを入力すると Webhook Events(Webhookイベント)が表示されるので、Incoming Messages(受信メッセージ)、Outgoing Messages(送信メッセージ)、または両方にチェックを入れたままにします。URLは後から追加・変更できます。
保存する
リスナーを追加 をクリックします。リスナーが Active(有効)ステータスのカードとして表示されます。URLを入力した場合は、そのURL用のWebhookシークレットが作成されます。
リスナーを管理する#
各カードには、番号、ステータス、Webhook URL:(または 未設定)、最終アクティビティ:(番号のメッセージが最後に確認された日時、または なし)、Webhookシークレット:(目のアイコンのボタンをクリックするまで非表示)が表示されます。
カードの ⋮ メニューを開くと、次の操作ができます。
| アクション | 動作 |
|---|---|
| 編集 | Webhook URL、Webhookイベント、シークレットを変更します。番号は変更できません。 |
| 無効にする / 有効にする | 無効にするとリスナーが Inactive(無効)になり、番号の受信トレイへの同期が止まり、Webhookも送信されなくなります。有効にすると再び Active になります。 |
| 削除 | 確認後にリスナーを削除します。すでに受信トレイにある会話は残ります。後で同じ番号を再度追加すると、リスナーが復元されます。再追加時にWebhook URLを入力すると、以前のWebhookシークレットがあれば、新しいシークレットは作成されずにそれが引き続き使われます。 |
リスナーのステータス#
| ステータス | 意味 |
|---|---|
| Active(有効) | 番号が接続されている間、メッセージが同期され、Webhookが送信されます。 |
| Pending(保留中) | Zapierなどでリスナーが作成されたときに、番号が接続されていませんでした。番号が接続されたら 有効にする をクリックしてください。 |
| Inactive(無効) | 無効化されています。何も同期されず、Webhookも送信されません。 |
統計#
| カード | 表示内容 |
|---|---|
| アクティブリスナー | 無効化されたものを含む、ページ上のすべてのリスナー。 |
| 利用可能番号 | 接続済みで、まだリスナーになっていない番号。リスナーを削除した番号もここでは使用済みとして数えられるため、実際に追加できる数より少なく表示されることがあります。 |
| 総制限 | Uniboxアドオンで作成できるリスナーの数。 |
| 今日のメッセージ | まだ集計されていません。常に 0 と表示されます。 |
Webhook URLまたはイベントを変更する#
リスナーを開く
カードの ⋮ をクリックし、編集 をクリックします。
設定を更新する
Webhook URL (Optional) と Webhook Events を変更します。この番号のWebhookを止めるには、URLを空にします。シークレットも削除され、URLを再度追加すると新しいシークレットが作成されます。
保存する
Update Listener(リスナーを更新)をクリックします。
シークレットを再生成する#
Edit Listener(リスナーを編集)で、Webhook Secret(Webhookシークレット)の隣にある更新ボタンをクリックして確認します。新しいシークレットはすぐに保存され、その後 Update Listener をクリックせずにダイアログを閉じても保存されたままです。以降のリクエストは新しいシークレットで署名されます。サーバーの設定をすぐに新しいシークレットに更新してください。
Webhookの配信の仕組み#
Wbiztoolは、番号の同期時に見つかった新しいメッセージごとに、URLへ POST リクエストを1件送信します。同期は、番号が接続されていてメッセージ送信中でない間、数分ごとに行われます。
- イベント: 相手からご自身の番号に届いたメッセージには
message_received、その番号から送信されたメッセージ(スマートフォン、キャンペーン、APIから)にはmessage_sentが使われます。送信されるのは、Webhook Events でチェックを入れたイベントのみです。 - Unibox受信トレイからの返信 は、同期の実行時にはすでに受信トレイにあるため、通常は
message_sentをトリガーしません。 - レスポンス: 8秒以内にHTTP
200を返してください。それ以外のレスポンスやタイムアウトは、配信失敗として扱われます。 - 再試行なし: 各メッセージは1回だけ送信されます。サーバーがダウンしていた場合、そのWebhookは失われます。
- 順序: リクエストは個別に送信されるため、順不同で届くことがあります。順序が重要な場合は
message.timestampで並べ替えてください。
ヘッダー#
| ヘッダー | 値 |
|---|---|
Content-Type | application/json |
X-Wbiztool-Event | message_received または message_sent |
X-Wbiztool-Timestamp | Webhookが送信された日時(ISO 8601、UTC)。ボディの timestamp と同じです。 |
X-Wbiztool-Webhook-Id | リスナーのID。ボディの webhook_id と同じです。 |
X-Wbiztool-Signature | sha256= の後に署名が続きます。リスナーにシークレットがある場合に送信され、URLが設定されていれば常にシークレットがあります。 |
ペイロード#
{
"event": "message_received",
"timestamp": "2026-09-16T10:31:12.482913+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0C1A2B3D4E5F60718",
"type": "chat",
"content": "Hi, is my order #4821 out for delivery?",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:29:58+00:00",
"whatsapp_timestamp": 1789554598,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_sent",
"timestamp": "2026-09-16T10:33:40.117205+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0F9E8D7C6B5A40312",
"type": "chat",
"content": "Yes, it will reach you today by 6 PM.",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:31:04+00:00",
"whatsapp_timestamp": 1789554664,
"direction": "outgoing",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_received",
"timestamp": "2026-09-16T10:36:02.904311+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0A7B6C5D4E3F20109",
"type": "image",
"content": "📸 Image",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:34:51+00:00",
"whatsapp_timestamp": 1789554891,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0,
"media": {
"filename": "",
"mimetype": "image/jpeg",
"size": 245760
}
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_received",
"timestamp": "2026-09-16T10:40:15.330187+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected][email protected]",
"type": "chat",
"content": "Is the store open on Sunday?",
"from": "120363041234567890",
"from_name": "Acme Loyalty Club",
"to": "919812345678",
"timestamp": "2026-09-16T10:39:02+00:00",
"whatsapp_timestamp": 1789555142,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "120363041234567890",
"name": "Acme Loyalty Club",
"is_group": true,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
},
"group": {
"name": "Acme Loyalty Club"
}
}上記の番号、ID、名前は例です。
トップレベルのフィールド#
| フィールド | 型 | 説明 |
|---|---|---|
event | string | message_received または message_sent。 |
timestamp | string | Webhookが送信された日時(ISO 8601、UTC)。 |
webhook_id | integer | リスナーのID。 |
whatsapp_client_id | string | WhatsApp設定に表示される、WhatsApp番号のID。 |
whatsapp_phone | string | ご自身のWhatsApp番号。 |
message | object | メッセージ。下記をご覧ください。 |
contact | object | 会話の相手の個人またはグループ。下記をご覧ください。 |
organisation | object | ワークスペースの id(string)と name。 |
group | object | グループチャットの場合のみ:name(グループ名)。 |
message のフィールド#
| フィールド | 型 | 説明 |
|---|---|---|
id | string | WhatsAppにおけるメッセージのID。重複を無視するために使用します。 |
type | string | テキストの場合は chat。それ以外はWhatsAppの種類で、image、video、audio、ptt(ボイスメッセージ)、document、sticker、location などです。 |
content | string | chat メッセージの場合はテキスト。メディアの場合は代わりにラベルが入ります:📸 Image、🎥 Video、🎵 Audio、🎤 Voice Message、😊 Sticker、Document: の後にドキュメントのテキスト(テキストがない場合は Document)、それ以外は Location のように先頭を大文字にした種類の名前。キャプションは含まれません。 |
from | string | 送受信のどちらでも、常に連絡先の番号(またはグループのID)です。 |
from_name | string | 連絡先またはグループの名前。空の場合もあります。 |
to | string | 送受信のどちらでも、常にご自身のWhatsApp番号 です。 |
timestamp | string | WhatsAppでメッセージが送信された日時(ISO 8601、UTC)。 |
whatsapp_timestamp | integer | 同じ日時を秒単位のUnixタイムスタンプで表したもの。 |
direction | string | incoming または outgoing。方向の判定には、from と to ではなくこのフィールドを使用してください。 |
status | string | 現在は常に pending です。配信状況や既読状況の判定には使わないでください。 |
is_forwarded | boolean | メッセージが転送されたものかどうか。 |
forwarding_score | integer | 転送された回数。 |
media | object | 詳細が取得できる場合のメディアメッセージ:filename、mimetype、size(バイト単位)。ファイル自体は含まれません。 |
quoted_message_id | string | メッセージが別のメッセージへの返信である場合のみ。 |
contact のフィールド#
| フィールド | 型 | 説明 |
|---|---|---|
whatsapp_id | string | WhatsApp ID。個人の場合は [email protected]、グループの場合は …@g.us のような形式です。 |
phone | string | + なしの番号。グループの場合はグループのID。 |
name | string | Wbiztoolが保持している連絡先の名前、またはグループ名。空の場合もあります。 |
is_group | boolean | グループチャットの場合は true。 |
is_business | boolean | WhatsApp Businessアカウントであることがわかっている場合は true。 |
署名を検証する#
各リクエストは、リスナーのシークレットを使ってHMAC-SHA256で署名されます。署名は 受信したままの生のリクエストボディ に対して計算され、sha256= と小文字の16進ダイジェストをつなげた値として X-Wbiztool-Signature で送信されます。
署名は必ず、JSONをパースする前の生のバイト列から計算してください。ボディをパースして再エンコードすると内容が変わり(たとえば、英語以外の文字や絵文字は \uXXXX としてエスケープされた状態で届きます)、署名が一致しなくなります。
// Express: keep the raw body for this route
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WBIZTOOL_WEBHOOK_SECRET;
app.post("/wbiztool/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const received = req.get("X-Wbiztool-Signature") || "";
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send("Invalid signature");
const data = JSON.parse(req.body.toString("utf8"));
if (data.event === "message_received") {
console.log(`New message from ${data.contact.phone}: ${data.message.content}`);
}
res.sendStatus(200); // reply quickly; do slow work in the background
});
app.listen(3000);# Flask
import hashlib
import hmac
import json
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"].encode()
@app.post("/wbiztool/webhook")
def wbiztool_webhook():
raw_body = request.get_data() # raw bytes, before parsing
expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
received = request.headers.get("X-Wbiztool-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
data = json.loads(raw_body)
if data["event"] == "message_received":
print(f"New message from {data['contact']['phone']}: {data['message']['content']}")
return "", 200<?php
$secret = getenv('WBIZTOOL_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
$received = $_SERVER['HTTP_X_WBIZTOOL_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($rawBody, true);
if ($data['event'] === 'message_received') {
error_log('New message from ' . $data['contact']['phone'] . ': ' . $data['message']['content']);
}
http_response_code(200);署名はタイムスタンプのヘッダーを対象にしていないため、リクエストのリプレイ(再送)攻撃は防げません。これが問題になる場合は、処理済みの message.id を保存し、重複したものを無視してください。
トラブルシューティング#
| メッセージまたは問題 | 対処方法 |
|---|---|
| Add New Listener(新しいリスナーを追加)をクリックすると Unibox Add-on Required(Uniboxアドオンが必要です)と表示される | ワークスペースにUniboxアドオンがありません。ダッシュボード内の請求・プランの 利用可能なアドオン から購入してください。 |
You have reached your unibox numbers limit | 不要になったリスナーを削除するか、アドオンの数量を増やしてください。 |
| No available WhatsApp numbers | 別の番号を接続してください。または、その番号はすでにリスナーになっています。 |
This WhatsApp number is already a listener | 代わりに既存のカードを編集してください。 |
Invalid WhatsApp client | 番号が切断されています。WhatsApp設定で再接続し、ページを再読み込みしてください。 |
保存時に value too long を含むエラーが表示される | Webhook URLが100文字を超えています。より短いURLを使用してください。 |
| Webhookが届かない | リスナーが Active であること、番号が接続されていること、イベントの種類にチェックが入っていること、URLが有効な証明書を持つ公開の https URLであることを確認してください。メッセージは次回の同期(数分後)の後にのみ送信されます。 |
| 一部のWebhookが届かない | サーバーが 200 以外を返したか、8秒以上かかったか、到達できませんでした。失敗した配信は再試行されません。 |
| 署名が一致しない | 再エンコードしたJSONではなく生のボディと、現在のシークレットを使用してください。シークレットを再生成するか、Zapierを接続すると、シークレットは置き換えられます。 |
| 最終アクティビティ: が なし のまま | 番号がまだ確認されていません。番号が接続されていて、メッセージ送信中でない必要があります。 |
