ThunderPhone 2.0 正式上线。全程自助,2 美分/分钟起。查看发布公告

Webhooks

Webhook 概述

了解 ThunderPhone 如何传送实时事件、如何验证签名,以及旧版与基于端点的传送模型如何比较。

ThunderPhone 会在通话期间发生事件时向您的服务器发送 HTTP POST 请求——例如呼入通话开始、通话结束、评分运行完成、触发警报等。提供 两种交付模型

事件目录中的全部十种事件类型都会通过 Webhook 端点 交付。六种通话生命周期事件 (telephony.incomingtelephony.completetelephony.toolweb.incomingweb.completeweb.tool)也会发送到 旧版单 URL Webhook——如果您同时配置了旧版 URL 和匹配的端点,您会在 两个路径上收到该事件。阻塞行为(telephony.incoming / web.incoming 配置 交换以及 Webhook 模式的 工具分派)仅存在于旧版路径; 所有端点交付均为即发即弃通知。

负载格式

端点交付的数据是一个包含 dataevent_idtype 的 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 发送相同的 typedata,但 不包含 event_id

{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}

在传输过程中,每个请求体均采用规范化序列化——键按字母顺序排序、 不含空白字符、使用 UTF-8。这些文档中的美化格式示例仅为便于阅读。

有关事件类型和负载字段的完整列表,请参阅事件目录

签名验证

每个请求都会在 X-ThunderPhone-Signature 请求头中携带针对原始请求体的 HMAC-SHA256 签名。签名密钥是端点的 secret(对于旧版投递,则是您组织级 Webhook 的 secret)。

步骤

  1. 在进行任何解析之前读取原始请求体。
  2. 计算 hmac_sha256(secret, body).hexdigest()
  3. 使用恒定时间比较方式与 X-ThunderPhone-Signature 请求头进行比较。

我们签名的是实际传输的字节,这些字节采用规范化 JSON 序列化格式(键已排序,分隔符紧凑)。因此,针对原始请求体进行验证始终有效——如果您的框架仅提供已解析的 JSON,使用已排序的键和紧凑分隔符重新序列化即可生成完全相同的字节。两种方法均在验证指南中介绍。

Python
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 "", 204
Node.js (Express)
import 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 网络钩子仅进行一次同步尝试,不会重试。

重试

每个事件都会立即尝试投递一次。任何 2xx 响应 都会确认投递。对于其他任何结果(非 2xx、 连接错误、超时),我们会在首次尝试后的 1 分钟、5 分钟、30 分钟、2 小时、6 小时、 12 小时和 24 小时进行重试——共 8 次尝试,覆盖 24 小时。如果每次尝试均失败,投递将停止,端点 会在网络钩子端点中标记为 status="failing"。载荷被持久化接收后,请尽快返回 2xx; 请异步处理。

排序

投递顺序尽力保证。实际上,我们会按照事件发出的 顺序投递,但失败后的重试可能会改变顺序。 始终按 call_id / 对象 ID 去重并核对。

重复

投递为至少一次:在我们未收到响应后进行重试, 可能会导致事件重复。每次重试都会携带相同的 event_id,因此请存储已处理的 ID 并跳过重复项。event_id 也会在端点之间共享——订阅同一事件的两个端点会收到相同的 event_id

超时

每次端点投递的超时时间为 30 秒。在 旧版路径上,驱动实时通话行为的阻塞请求—— telephony.incoming / web.incoming 配置交换——会在 10 秒后超时,但较慢的响应会延迟通话接听,因此应在几秒钟内 响应。网络钩子模式的工具调度默认允许 20 秒, 工具声明可设置顶级 timeout

源 IP

出站网络钩子来自 ThunderPhone 的云 IP 范围。 如果您的防火墙需要允许列表,请联系支持团队,我们将 分享当前的范围。

在旧版网络钩子与基于端点的网络钩子之间进行选择

功能旧版(/v1/webhook端点(/v1/developer/webhook-endpoints
URL 数量每个组织 1 个每个组织多个
事件覆盖范围telephony.* / web.*所有 10 种事件类型
事件筛选每个端点单独设置
重试24 小时内 8 次尝试
封装type + datatype + data + event_id
密钥轮换替换单个密钥每个端点单独的密钥
无需删除即可禁用status=disabled
状态可见性active / disabled / failing
阻塞式配置交换是(telephony.incoming / web.incoming从不——仅通知
最适合动态通话配置生产环境中的事件消费

新集成应通过基于端点的 网络钩子接收事件。仅当您在接听时动态配置通话, 或使用网络钩子模式工具调度时,才保留(或添加)旧版 URL——这些 请求/响应交换仅在旧版路径上运行。


相关内容