Webhookの概要
ThunderPhoneがリアルタイムイベントを配信する仕組み、署名を検証する方法、従来型とエンドポイントベースの配信モデルの比較。
ThunderPhoneは、通話中にイベントが発生するとサーバーへ HTTP POST リクエストを送信します。たとえば、着信通話の開始、通話の終了、評価実行の完了、アラートの発火などです。配信モデルは2種類あります。
複数の URL、エンドポイントごとのシークレット、エンドポイントごとのイベントフィルター、自動再試行を利用できます。
GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints で管理します。
組織ごとに1つの URL。ブロッキング設定交換を含む通話ライフサイクルイベントを送信します。GET/PUT /v1/webhook で管理します。
イベントカタログの10種類すべてのイベントは、Webhook エンドポイントを通じて配信されます。6種類の通話ライフサイクルイベント(telephony.incoming、telephony.complete、telephony.tool、web.incoming、web.complete、web.tool)は、レガシーの単一 URL Webhookにも同時に送信されます。レガシー URL と一致するエンドポイントの両方がある場合、イベントは両方のパスで受信します。ブロッキング動作(telephony.incoming / web.incoming の設定交換およびWebhookモードのツールディスパッチ)はレガシーパス専用です。すべてのエンドポイント配信は、応答を待たない通知です。
ペイロード形式
エンドポイントへの配信は、data、event_id、type を含む JSON オブジェクトです。
{
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
},
"event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
"type": "telephony.incoming"
}event_id は送信されたイベントごとに一意です。再試行時も、イベントを受信するすべてのエンドポイント間でも同一です。この値で重複を排除してください。
レガシーの単一 URL Webhookは同じ type と data を送信しますが、event_id は含まれません。
{
"type": "telephony.incoming",
"data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}通信上では、すべての本文が正規形式でシリアル化されます。キーはアルファベット順に並び、空白は含まれず、UTF-8 が使用されます。このドキュメント内の整形済みの例は、読みやすさのためだけに掲載しています。
イベントタイプとペイロードフィールドの完全な一覧は、イベントカタログを参照してください。
署名の検証
すべてのリクエストには、生のリクエスト本文
に対する HMAC-SHA256 署名が X-ThunderPhone-Signature ヘッダーに含まれます。署名キーは
エンドポイントの secret です(レガシー配信では組織レベルの webhook secret)。
手順
- 解析を行う前に、生のリクエスト本文を読み取ります。
hmac_sha256(secret, body).hexdigest()を計算します。X-ThunderPhone-Signatureヘッダーと定数時間で比較します。
ThunderPhone は送信するバイト列をそのまま署名します。そのバイト列は 正規化された JSON シリアライズ(キーをソートし、区切り文字を圧縮)です。そのため、 生の本文に対する検証は常に機能します。また、フレームワークから解析済み JSON のみが渡される場合でも、 キーをソートし区切り文字を圧縮して再シリアライズすれば、同一のバイト列が生成されます。どちらの方法も 検証ガイドで説明しています。
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature or "")
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/thunderphone-webhook")
def handle():
body = request.get_data()
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify_signature(body, sig, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
# dispatch on event["type"] …
return "", 204import crypto from "node:crypto";
import express from "express";
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (!signature || expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
const app = express();
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("X-ThunderPhone-Signature") || "";
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// dispatch on event.type …
res.sendStatus(204);
},
);配信セマンティクス
これらのセマンティクスはエンドポイント配信に適用されます。従来の単一 URL Webhook は、再試行なしの単一同期試行です。
再試行
各イベントは即座に 1 回試行されます。2xx 応答はすべて
配信を確認します。それ以外の結果(非 2xx、
接続エラー、タイムアウト)の場合、最初の試行から 1 分後、5 分後、30 分後、2 時間後、6 時間後、
12 時間後、24 時間後に再試行します。つまり、
24 時間にわたって 8 回試行します。すべての試行が失敗すると、配信は停止され、エンドポイントは
Webhook エンドポイントで
status="failing" とマークされます。ペイロードを永続的に受け入れたら、できるだけ早く 2xx を返してください。処理は非同期で行います。
順序
配信順序はベストエフォートです。実際にはイベントが発行された順に配信しますが、
失敗時の再試行によって順序が変わる可能性があります。
常に call_id / オブジェクト ID で重複排除と照合を行ってください。
重複
配信は少なくとも 1 回です。確認できなかった応答後の再試行により、
イベントが重複することがあります。すべての再試行には同じ
event_id が含まれるため、処理済み ID を保存し、重複をスキップしてください。event_id は
エンドポイント間でも共有されます。同じイベントを購読している 2 つのエンドポイントは、同じ event_id を受け取ります。
タイムアウト
エンドポイント配信では、試行ごとに 30 秒のタイムアウトがあります。従来のパスでは、
ライブ通話の動作を制御するブロッキングリクエスト、つまり
telephony.incoming / web.incoming
の設定交換は 10 秒後にタイムアウトします。ただし、応答が遅いと通話の応答が遅延するため、数秒以内の応答を目指してください。Webhook モードのツールディスパッチは
デフォルトで 20 秒を許可し、ツール宣言ではトップレベルの timeout を設定できます。
送信元 IP
送信 Webhook は ThunderPhone のクラウド IP 範囲から発信されます。 ファイアウォールで許可リストが必要な場合は、サポートにお問い合わせください。最新の範囲を共有します。
従来型 Webhook とエンドポイントベース Webhook の選択
| 機能 | 従来型(/v1/webhook) | エンドポイント(/v1/developer/webhook-endpoints) |
|---|---|---|
| URL 数 | 組織ごとに 1 件 | 組織ごとに複数 |
| イベント対象範囲 | telephony.* / web.* のみ | 全 10 イベントタイプ |
| イベントフィルター | — | エンドポイントごと |
| 再試行 | なし | 24 時間で 8 回試行 |
| エンベロープ | type + data | type + data + event_id |
| シークレットローテーション | 単一シークレットを置換 | エンドポイントごとのシークレット |
| 削除せずに無効化 | — | status=disabled |
| ステータスの可視性 | — | active / disabled / failing |
| ブロッキング設定交換 | はい(telephony.incoming / web.incoming) | なし — 通知のみ |
| 最適な用途 | 動的な通話設定 | 本番環境でのイベント処理 |
新しい統合では、エンドポイントベースの Webhook を通じてイベントを処理してください。通話応答時に通話を動的に設定する場合、または Webhook モードのツールディスパッチを使用する場合にのみ、従来の URL を維持または追加してください。これらのリクエスト/レスポンス交換は従来のパスでのみ実行されます。