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

Webhooks

Webhook 概览

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

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

事件目录中列出的所有事件类型都会通过 Webhook 端点交付。六种通话生命周期事件(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。这些文档中的美化示例仅用于提升可读性。

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

签名验证

每个请求都带有两个签名,二者均使用端点的 secret(对于旧版投递,则使用您的组织级 webhook secret)作为密钥,通过 HMAC-SHA256 生成:

  • X-ThunderPhone-Signature-V2: t=<unix seconds>,v1=<hex> 对时间戳、方法、路径和查询参数以及原始请求体进行签名。请验证此签名,并拒绝 t 与您的时钟相差超过 5 分钟 的请求,以防截获的请求随后被重放。重试请求会在发送时重新签名。
  • X-ThunderPhone-Signature 仅对原始请求体进行签名。该签名仍会原样发送,以确保现有验证器能够继续正常工作。

步骤

  1. 在进行任何解析之前,读取请求头、方法、原始路径和查询参数,以及原始请求体。
  2. 使用您的密钥计算 <t>\n<METHOD>\n<path and query>\n<body> 的十六进制 HMAC-SHA256。
  3. 如果 t 的偏差超过 300 秒,或结果在恒定时间比较中与 v1 不匹配,则拒绝该请求。

请求体采用规范化 JSON 序列化格式(键已排序、分隔符紧凑),并且我们签名的字节与传输的字节完全一致。验证指南提供了精确的签名字符串、完整示例、不使用 SDK 的代码以及旧版请求头。

Python (Flask)
from flask import Flask, abort, request
from thunderphone import verify_signature
 
app = Flask(__name__)
 
@app.post("/thunderphone-webhook")
def handle():
    query = request.query_string.decode()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=WEBHOOK_SECRET,
        method=request.method,
        path=request.path + (f"?{query}" if query else ""),
        body=request.get_data(),  # raw bytes
    ):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
Node.js (Express)
import express from "express";
import { verifySignature } from "@thunderphone/sdk";
 
const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl,
      body: req.body,
    });
    if (!ok) return res.sendStatus(401);
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type …
    res.sendStatus(204);
  },
);

投递语义

这些语义适用于 端点 投递。旧版单 URL webhook 仅进行一次同步尝试且不重试,但电话 telephony.incoming 配置交换除外,该交换会重试一次。

重试

每个事件都会立即尝试投递一次。任何 2xx 响应 都会确认投递。对于任何其他结果(非 2xx、 连接错误、超时),我们会在首次尝试后的 1 分钟、5 分钟、30 分钟、2 小时、6 小时、 12 小时和 24 小时进行重试——共 8 次尝试,持续 24 小时。如果每次尝试都失败,投递将停止,端点 会在webhook 端点中标记为 status="failing";我们会向您组织的 所有者和管理员发送电子邮件,并在控制台中显示该状态。请在 有效负载被持久化接收后尽快返回 2xx;请异步处理。 来自端点测试 API 的测试发送会在首次尝试约一分钟后重试一次, 且绝不会更改端点状态。

顺序

投递顺序采用尽力而为的方式。实际上,我们会按照事件发出的顺序进行投递,但失败后的重试可能会改变顺序。 请始终按 call_id / 对象 ID 去重并进行协调。

重复

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

超时

端点投递每次尝试的超时为 30 秒。在 旧版路径中,驱动实时通话行为的阻塞请求—— telephony.incoming / web.incoming 配置交换——电话首次尝试的时间限制为 10 秒, 整个交换过程(包括其一次 重试)限制为 13 秒,而小组件会话限制为 10 秒。响应缓慢会延迟 电话接听,因此请尽量在几秒内响应。Webhook 模式的工具调度 默认允许 20 秒,工具声明可以设置顶层 timeout。

源 IP

出站 Webhook 源自 ThunderPhone 的云 IP 范围。 如果您的防火墙需要允许列表,请联系支持团队,我们将提供当前范围。

在传统 Webhook 与基于端点的 Webhook 之间选择

功能旧版(/v1/webhook)端点(/v1/developer/webhook-endpoints)
URL 数量每个组织 1 个每个组织多个
事件覆盖范围仅 telephony.* / web.*全部 10 种事件类型
事件筛选——按端点筛选
重试无,电话通话配置交换除外,可重试 1 次24 小时内尝试 8 次
信封格式type + datatype + data + event_id
密钥轮换替换单个密钥每个端点独立密钥
不删除即可停用使用 PUT /v1/webhook 并传入 {"url": ""}status=disabled
状态可见性——active / disabled / failing
阻塞式配置交换是(telephony.incoming / web.incoming)从不——仅通知
最适用于动态通话配置生产环境中的事件消费

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


相关内容