---
title: "Webhook 概览"
description: "了解 ThunderPhone 如何传递实时事件、如何验证签名，以及旧版与基于端点的传递模型之间的比较。"
---

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

<CardGroup cols={2}>
  <Card title="Webhook 端点（推荐）" icon="bolt" href="/zh/webhooks/endpoints">
    支持多个 URL、每个端点独立的密钥、每个端点独立的事件筛选器，以及自动重试。
    通过 `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints` 管理。
  </Card>
  <Card title="单 URL 旧版 Webhook" icon="link" href="/api-reference/organizations#legacy-single-url-webhook">
    每个组织一个 URL。承载通话生命周期事件，包括**阻塞式**配置交换。通过 `GET/PUT /v1/webhook` 管理。
  </Card>
</CardGroup>

[事件目录](/zh/webhooks/events)中列出的所有事件类型都会通过 Webhook 端点交付。六种通话生命周期事件（`telephony.incoming`、`telephony.complete`、`telephony.tool`、`web.incoming`、`web.complete`、`web.tool`）**也会**发送到旧版单 URL Webhook——如果您同时配置了旧版 URL 和匹配的端点，您会在**两条**路径上收到该事件。阻塞行为（[`telephony.incoming` / `web.incoming` 配置交换](/zh/webhooks/call-incoming)以及 Webhook 模式的[工具分派](/zh/tools/overview)）仅存在于旧版路径；每次端点交付都是即发即弃的通知。

## 负载格式

端点交付的数据是一个包含 `data`、`event_id` 和 `type` 的 JSON 对象：

```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`：

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

在线路传输中，每个请求体都会以规范化方式序列化——键按字母顺序排序、无空白字符、使用 UTF-8。这些文档中的美化示例仅用于提升可读性。

请参阅[事件目录](/zh/webhooks/events)，了解完整的事件类型和负载字段列表。

## 签名验证

每个请求都带有两个签名，二者均使用端点的 `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 序列化格式（键已排序、分隔符紧凑），并且我们签名的字节与传输的字节完全一致。[验证指南](/zh/guides/verify-webhook-signatures)提供了精确的签名字符串、完整示例、不使用 SDK 的代码以及旧版请求头。

<CodeGroup>
```python 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
```

```javascript 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);
  },
);
```
</CodeGroup>

## 投递语义

这些语义适用于 **端点** 投递。旧版单 URL
webhook 仅进行一次同步尝试且不重试，但电话
[`telephony.incoming`](/zh/webhooks/call-incoming)
配置交换除外，该交换会重试一次。

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

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

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

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

  <Accordion title="源 IP">
    出站 Webhook 源自 ThunderPhone 的云 IP 范围。
    如果您的防火墙需要允许列表，请联系支持团队，我们将提供当前范围。
  </Accordion>
</AccordionGroup>

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

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

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

---

## 相关内容

<CardGroup cols={2}>
  <Card title="事件目录" icon="list" href="/zh/webhooks/events">
    所有事件类型及其负载。
  </Card>
  <Card title="Webhook 端点" icon="bolt" href="/zh/webhooks/endpoints">
    管理多个端点、事件筛选器和密钥。
  </Card>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/zh/webhooks/call-incoming">
    您的服务器必须响应的阻塞请求，用于配置通话。
  </Card>
  <Card title="telephony.complete / web.complete" icon="phone" href="/zh/webhooks/call-complete">
    通话结束后的负载，包含转录文本、录音和指标。
  </Card>
</CardGroup>
