Skip to main content
我们发送到您服务器的每个请求——webhook 投递和工具端点调用——都会在 X-ThunderPhone-Signature 请求头中携带 HMAC-SHA256 签名。一次正确实现验证,然后将同一个辅助函数接入每个处理程序。

算法

  1. 读取原始请求正文——即我们 POST 给您的精确字节。
  2. 计算 hmac_sha256(secret, body).hexdigest()
  3. X-ThunderPhone-Signature 进行恒定时间比较。 (简单的字符串比较会泄露时序信息。)
我们对传输的精确字节进行签名,因此验证原始正文始终有效。这些字节也是载荷的规范 JSON 序列化——键按字母顺序排序、紧凑分隔符(,:,不含空格)、UTF-8。当您的框架仅提供已解析的 JSON 时,您可以使用第二种完全等效的方法:进行规范化重新序列化,然后对其计算 HMAC。
优先使用原始正文——这样少一步,并且不会受到某些语言中 JSON 数字往返转换细节的影响。

使用哪个密钥?

将密钥存储在您的密钥管理器或环境变量中——切勿提交到代码仓库。

参考实现

以下四种实现均验证原始请求正文:

特定框架接入

验证工具调用

当智能体直接调用您的某个 函数工具(该工具具有 endpoint)时,请求会在您配置的 endpoint.headers 之外携带两个 ThunderPhone 请求头:
  • X-ThunderPhone-Call-ID —— 进行中通话的数字 ID。
  • X-ThunderPhone-Signature —— 使用您的组织级 webhook 密钥作为密钥,对精确的请求正文字节计算的 HMAC-SHA256。
同一个 verify() 辅助函数无需修改即可使用,但有两点需要注意:
  1. GET / DELETE 工具没有正文。 参数通过查询参数传递,签名基于空字节字符串计算——因此使用 verify(b"", sig, secret)(Python)或 verify(Buffer.alloc(0), sig, secret)(Node)。请对查询字符串进行哈希。
  2. 未配置旧版 webhook 的组织没有组织密钥。 在这种情况下,工具调用仅携带 X-ThunderPhone-Call-ID,不包含签名请求头。请配置旧版 webhook(PUT /v1/webhook)以获取签名密钥,或通过 endpoint.headers 中您自己的请求头对工具调用进行身份验证。
Webhook 模式的工具分发(没有 endpoint 的工具,会以 telephony.tool / web.tool 的形式发送到您的组织 webhook)属于普通的已签名 webhook——适用上述标准方法。有关两种请求形式,请参阅函数工具

常见陷阱

解析请求体后,再使用 JSON 库的默认设置重新导出(在 , / : 后添加空格、按插入顺序排列键)会生成不同的字节,从而导致 HMAC 验证失败。请验证原始请求体——或者如果必须重新序列化,请严格匹配我们的规范格式:键排序、紧凑分隔符、UTF-8。
Express 的 express.json() 中间件会消耗请求体流,导致您丢失原始字节。请仅在 webhook 路由上使用 express.raw(),或者在前置中间件中缓冲原始请求体。NestJS / Koa 也是如此——请查阅它们关于“原始请求体”的文档。
JS 中的 expected === signature 或 Python 中的 expected == signature 都是时序可变比较。请分别使用 crypto.timingSafeEqualhmac.compare_digest。性能差异可以忽略不计。
直接调用工具端点时,签名使用的是组织级 webhook 密钥GET /v1/webhook)——而不是 /v1/developer/webhook-endpoints 中任何单个端点的密钥。复用相同的 verify() 函数,但请确保在工具路由中传入组织密钥。
对于没有请求体的工具方法,签名覆盖空字节字符串,从而保持一套通用规则:对原始请求体进行 HMAC,无论其内容是什么。对 URL 或查询字符串进行哈希永远不会匹配。
验证失败时返回 200 会使处理程序成为重放攻击目标。如果验证失败,始终返回非 2xx 响应。

后续步骤

Webhook 概览

投递语义、重试、源 IP 地址。

Webhook 端点

管理多个 URL、轮换密钥。

函数工具

两种工具调用路径及其请求格式。

工具集成

端到端构建完整的工具支持型集成。