# 使用 Webhook 接收事件通知

> 全部文档的索引见 [llms.txt](https://ark.volcengine.com/llms.txt)；在任一文档页面地址后追加 `.md` 可获取该页的 Markdown 版本。

如果你需要把 Agent 运行结果接入审计、告警或内部编排系统，不必为每个资源持续轮询状态。配置 Managed Agents Webhook 后，方舟会在 Agent、Environment、Session、Memory Store 和 Vault 等资源发生变化时主动回调你的服务端。本指南从创建接入点开始，串起接收服务、验签去重、测试事件、故障恢复、密钥轮换和资源清理。

<span id="choose-receiving-method"></span>
## 选择接收方式

Webhook、SSE 和轮询解决的问题不同。需要监听多个资源的异步变化时，使用 Webhook。需要实时展示单个 Session 的消息、工具调用和增量输出时，使用 SSE。需要主动核对资源当前状态或补查 Session 历史事件时，调用对应的查询接口。

| 维度 | Webhook | SSE | 轮询 |
| --- | --- | --- | --- |
| 适用范围 | 跨资源接收生命周期和状态变化 | 实时接收指定 Session 或线程的细粒度事件 | 主动查询资源当前状态或历史记录 |
| 连接方式 | 平台向公网回调地址发送 HTTP POST | 客户端维持长连接 | 客户端按需发送查询请求 |
| 成功判定 | 接收端返回 HTTP 2xx | 客户端持续读取事件流 | 查询接口返回成功响应 |
| 可靠性处理 | 验签、去重、重试和停用恢复 | 断线后重连，并按接口能力补查 | 控制查询频率，避免高频轮询 |
| 推荐场景 | 审计、告警、异步任务编排 | 实时对话、运行进度展示 | 对账、恢复和低频状态检查 |

Webhook 支持订阅下表中的业务事件。事件正文结构详情请参见 [Webhook 事件结构参考](/docs/webhook-events-reference)。

| 资源域 | 事件类型 |
| --- | --- |
| Session 与线程 | `session.status_run_started`、`session.status_idled`、`session.status_terminated`、`span.outcome_evaluation_end`、`session.error`、`session.thread_status_running`、`session.thread_status_idle`、`session.thread_status_terminated`、`session.updated`、`session.deleted` |
| Agent | `agent.created`、`agent.updated`、`agent.deleted` |
| Environment | `environment.created`、`environment.updated`、`environment.deleted` |
| Memory Store | `memory_store.created`、`memory_store.deleted` |
| Vault 与凭证 | `vault.created`、`vault.deleted`、`vault_credential.created`、`vault_credential.deleted`、`vault_credential.refresh_failed` |

> **注意**
>
> Webhook 事件查询只返回最近 7 天内与接入点关联的冻结事件信封，不返回投递状态或逐次重试明细，也不提供事件重放。接收端仍应自行持久化事件和处理结果。发现通知缺失时，调用对应资源的查询接口核对当前状态；Session Events 查询接口不是 Webhook 历史接口。

<span id="determine-session-result"></span>
### 判定 Session 运行结果

`session.status_idled` 只表示 Session 已进入空闲状态，不表示当前轮次正常结束。Webhook 事件不携带 Session Event ID 或轮次 ID，不能只查询最近一条 `session.status_idle` 后自动推进业务。收到该通知后，按以下顺序判定结果：

1. 从 Webhook 事件的 `data.id` 读取 Session ID，并按 Webhook 外层 `id` 去重。
2. 找到业务侧记录的本轮边界。推荐在发送本轮 `user.message`、`user.tool_result` 或 `user.tool_confirmation` 后，保存发送事件接口返回的事件 ID；如果接收端已经持续消费 Session 事件，也可以保存上一条已处理 Session Event 的 `processed_at` 作为水位。
3. 使用 Session ID 调用 [查询会话事件列表](/docs/list-events-api)，查询本轮边界之后的历史事件。可以用 `created_at[gt]` 缩小时间窗口，并按 `processed_at` 正序扫描；只有确认候选 `session.status_idle` 位于本轮边界之后，才将它作为本次通知对应的状态事件。
4. 读取对应 `session.status_idle` 事件的 `stop_reason.type`：
    - `end_turn`：当前轮次正常结束，可以处理业务结果。
    - `requires_action`：当前轮次正在等待工具确认或工具结果，按 `stop_reason.event_ids` 继续处理。
    - `retries_exhausted`：当前轮次重试次数已用尽，不要按成功结果推进后续流程。

Webhook 可能早于 Session 历史事件完成同步，也可能延迟投递。若查询结果中只有本轮边界之前的 `session.status_idle`，或无法确认候选事件与本轮业务任务的关系，等待后继续查询；不能仅凭最近一条 `end_turn` 推进业务。无法建立本轮边界时，`session.status_idled` 只适合作为状态核对信号，不适合驱动自动发布、扣款或外部通知等必须精确对应轮次的流程。

Webhook 外层 `id` 以 `whe-` 开头，只标识 Webhook 事件信封。Session 历史事件使用独立的事件 ID，不能将 `whe-` ID 传给 Session Events 查询接口。Session 事件字段详情请参见 [会话事件结构参考](/docs/sessions-events-reference-v2)。

<span id="prepare-receiver"></span>
## 准备接收端

开始前，完成以下准备：

- 创建方舟 API Key，并配置为 `ARK_API_KEY`。
- 准备一个公网可访问的 HTTP 或 HTTPS 回调地址。平台不跟随 3xx 重定向。
- 安装 Python 3.10 或更高版本。
- 确定需要订阅的事件类型。创建接入点时至少填写 1 种，可填写全部支持的事件类型。
- 为生产环境准备密钥管理服务和持久化去重存储。

本指南使用 `/webhooks/managed-agents` 作为回调路径。Python 示例只依赖标准库，通过 `HTTPServer` 监听 `0.0.0.0:8000`。

> **注意**
>
> 标准库 `HTTPServer` 示例用于演示协议处理，不包含 TLS、访问日志脱敏、限流、多进程并发和持久化存储。生产环境应在反向代理或 API 网关后运行接收服务，并将回调地址配置为 HTTPS。

<span id="create-and-test-endpoint"></span>
## 创建并测试接入点

<span id="create-endpoint"></span>
### 第 1 步：创建接入点

配置 `ARK_API_KEY`，并将示例中的 `url` 替换为公网回调地址，然后创建 Webhook 接入点。接入点创建成功后默认处于 `ACTIVE` 状态。

```Bash
curl -sS --fail-with-body "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints" \
  -H "Authorization: Bearer $ARK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/managed-agents",
    "description": "接收 Session 空闲和终止通知",
    "events": [
      "session.status_idled",
      "session.status_terminated"
    ]
  }'
```

创建成功后，响应中的 `id` 以 `wep-` 开头，`signing_secret` 以 `whsec_` 开头。`signing_secret` 仅在本次创建响应中返回一次。

立即将两个值保存到服务端配置：

```Bash
export WEBHOOK_ENDPOINT_ID="<WEBHOOK_ENDPOINT_ID>"
export WEBHOOK_SIGNING_SECRET="<SIGNING_SECRET>"
```

> **警告**
>
> 关闭创建响应前，将 `signing_secret` 写入密钥管理服务。列表和详情接口不会再次返回该密钥。生产环境不要把密钥提交到代码仓库、镜像、日志或 shell 历史。

> **注意**
>
> 同一地域、账号和项目内最多创建 20 个接入点，启用和停用状态均计入配额。同一项目内回调 URL 必须唯一。管理 API 不支持 `Idempotency-Key`；创建请求超时后，先按 URL 查询接入点列表，不要直接重复创建。

<span id="start-receiver"></span>
### 第 2 步：启动接收端

Python 示例先读取原始请求体，完成时间窗口和签名校验后再解析 JSON。验签成功后，示例按事件 ID 在内存中去重，并返回 HTTP 204。

```Python
import base64
import binascii
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer


WEBHOOK_PATH = "/webhooks/managed-agents"
MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60
PROCESSED_EVENT_IDS: set[str] = set()


def decode_signing_secret(value: str) -> bytes:
    if not value.startswith("whsec_"):
        raise ValueError("WEBHOOK_SIGNING_SECRET must start with whsec_")

    encoded_secret = value.removeprefix("whsec_")
    encoded_secret += "=" * (-len(encoded_secret) % 4)
    try:
        secret = base64.b64decode(encoded_secret, validate=True)
    except (binascii.Error, ValueError) as exc:
        raise ValueError("WEBHOOK_SIGNING_SECRET contains invalid Base64 data") from exc
    if len(secret) != 32:
        raise ValueError("WEBHOOK_SIGNING_SECRET must decode to 32 bytes")
    return secret


def verify_signature(
    secret: bytes,
    webhook_id: str,
    timestamp: str,
    raw_body: bytes,
    signature_header: str,
) -> bool:
    signed_content = (
        webhook_id.encode("utf-8")
        + b"."
        + timestamp.encode("utf-8")
        + b"."
        + raw_body
    )
    digest = hmac.new(secret, signed_content, hashlib.sha256).digest()
    expected_signature = base64.b64encode(digest).decode("ascii")

    version, separator, signature = signature_header.partition(",")
    return bool(
        separator
        and version == "v1"
        and hmac.compare_digest(signature, expected_signature)
    )


class WebhookHandler(BaseHTTPRequestHandler):
    signing_secret: bytes

    def send_status(self, status_code: int, message: str | None = None) -> None:
        response_body = b""
        if message is not None:
            response_body = json.dumps({"error": message}).encode("utf-8")

        self.send_response(status_code)
        if response_body:
            self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(response_body)))
        self.end_headers()
        if response_body:
            self.wfile.write(response_body)

    def do_POST(self) -> None:
        if self.path != WEBHOOK_PATH:
            self.send_status(404, "Webhook path not found")
            return

        content_length = self.headers.get("Content-Length")
        try:
            body_length = int(content_length or "")
        except ValueError:
            self.send_status(400, "Invalid Content-Length")
            return
        if body_length < 0:
            self.send_status(400, "Invalid Content-Length")
            return

        raw_body = self.rfile.read(body_length)
        webhook_id = self.headers.get("webhook-id", "")
        timestamp = self.headers.get("webhook-timestamp", "")
        signature = self.headers.get("webhook-signature", "")
        if not webhook_id or not timestamp or not signature:
            self.send_status(400, "Missing webhook signature headers")
            return

        try:
            timestamp_seconds = int(timestamp)
        except ValueError:
            self.send_status(400, "Invalid webhook timestamp")
            return

        if abs(time.time() - timestamp_seconds) > MAX_TIMESTAMP_SKEW_SECONDS:
            self.send_status(401, "Webhook timestamp is outside the allowed window")
            return

        if not verify_signature(
            self.signing_secret,
            webhook_id,
            timestamp,
            raw_body,
            signature,
        ):
            self.send_status(401, "Invalid webhook signature")
            return

        try:
            event = json.loads(raw_body)
        except (json.JSONDecodeError, UnicodeDecodeError):
            self.send_status(400, "Invalid JSON body")
            return

        if not isinstance(event, dict):
            self.send_status(400, "Webhook body must be a JSON object")
            return

        event_id = event.get("id")
        if (
            not isinstance(event_id, str)
            or not event_id.startswith("whe-")
            or event_id != webhook_id
        ):
            self.send_status(400, "Webhook event ID does not match the request header")
            return

        if event_id in PROCESSED_EVENT_IDS:
            self.send_status(204)
            return

        data = event.get("data")
        event_type = data.get("type") if isinstance(data, dict) else None
        print(f"Received event {event_id}: {event_type}", flush=True)
        PROCESSED_EVENT_IDS.add(event_id)
        self.send_status(204)


def main() -> None:
    secret_value = os.environ.get("WEBHOOK_SIGNING_SECRET")
    if not secret_value:
        raise SystemExit("Set WEBHOOK_SIGNING_SECRET before starting the server")

    try:
        WebhookHandler.signing_secret = decode_signing_secret(secret_value)
    except ValueError as exc:
        raise SystemExit(str(exc)) from exc

    server = HTTPServer(("0.0.0.0", 8000), WebhookHandler)
    print(f"Listening on http://0.0.0.0:8000{WEBHOOK_PATH}", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()
```

配置创建响应中的签名密钥，然后启动服务：

```Bash
export WEBHOOK_SIGNING_SECRET="<SIGNING_SECRET>"
python code/managed-agents/managed-agents-receive-webhook-python.py
```

启动成功后，终端显示以下地址：

```text
Listening on http://0.0.0.0:8000/webhooks/managed-agents
```

将该服务部署到创建接入点时填写的公网回调地址。回调路径必须保持一致。

<span id="send-test-event"></span>
### 第 3 步：发送测试事件

发送一个 `webhook.test` 事件，验证公网路由、请求头、验签和响应状态。

```Bash
curl -sS --fail-with-body -X POST \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/test" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

测试接口返回 HTTP 200 表示平台已经生成并受理测试事件，不表示接收端已经收到事件。测试事件通过异步任务投递一次，不受订阅列表和接入点启停状态影响，也不会触发重试。普通接收失败不改变接入点状态。

每次调用都会生成新的测试事件。请求超时后直接重试可能导致接收端收到多个不同事件 ID 的测试通知。

接收端收到事件后输出：

```text
Received event whe-20261012080500-f6g7h: webhook.test
```

确认日志中的 `whe-` ID 与测试接口响应中的 `id` 相同。Python 示例处理成功后返回 HTTP 204。

<span id="verify-and-process"></span>
## 验签并处理事件

每次回调包含以下签名请求头：

| 请求头 | 说明 |
| --- | --- |
| `Content-Type` | 固定为 `application/json`，正文使用 UTF-8 编码 |
| `User-Agent` | 固定为 `ArkManagedAgents-Webhook/1.0` |
| `webhook-id` | Webhook 事件 ID，以 `whe-` 开头，与正文外层 `id` 相同 |
| `webhook-timestamp` | 生成签名时使用的 Unix 秒时间戳 |
| `webhook-signature` | `v1,<BASE64_SIGNATURE>` 格式的签名 |

签名内容按以下顺序拼接：

```text
signed_content = webhook-id + "." + webhook-timestamp + "." + raw_body
signature = Base64(HMAC-SHA256(Base64Decode(signing_secret 去掉 whsec_ 前缀), signed_content))
```

其中，`webhook-id` 和 `webhook-timestamp` 使用 UTF-8 编码。`raw_body` 必须是网络中收到的原始字节。

按以下顺序处理请求：

1. 读取 `webhook-id`、`webhook-timestamp`、`webhook-signature` 和原始请求体。
2. 将 `webhook-timestamp` 解析为 Unix 秒，并拒绝与服务端当前时间相差超过 5 分钟的请求。
3. 去掉 `signing_secret` 的 `whsec_` 前缀，再对剩余内容进行 Base64 解码。解码结果必须为 32 字节。
4. 使用 `webhook-id.timestamp.raw_body` 计算 HMAC-SHA256 和 Base64 签名。
5. 校验签名版本为 `v1`，并使用 `hmac.compare_digest` 等恒定时间比较函数比较 Base64 签名。
6. 验签通过后解析 JSON，并确认正文外层 `id` 与 `webhook-id` 相同。
7. 按外层 `id` 去重。重复收到同一个 `whe-` ID 时，不要重复执行有副作用的业务操作。
8. 可靠保存事件或业务处理结果后，返回 HTTP 2xx。

> **注意**
>
> 不要先解析 JSON，再重新序列化正文参与验签。空格、字段顺序或转义方式变化都会改变字节序列，导致合法请求验签失败。

Python 示例使用内存集合去重，进程重启后记录会丢失。生产环境应在数据库或缓存中为事件 ID 建立唯一约束，并将去重记录的保留时间覆盖业务追溯周期。

<span id="delivery-rules"></span>
### 投递和重试

业务事件不保证严格顺序或恰好一次投递。同一事件重试时，`webhook-id`、`created_at` 和原始正文保持不变。平台会为每次尝试重新生成 `webhook-timestamp` 和 `webhook-signature`。

| 规则 | 行为 |
| --- | --- |
| 成功响应 | 接收端返回任意 HTTP 2xx 后，本次投递成功；平台忽略响应正文 |
| 单次超时 | DNS、连接、TLS、发送和读取的总时间最多为 5 秒 |
| 最大次数 | 同一业务事件最多投递 3 次 |
| 重试间隔 | 第一次失败后等待 5 秒，第二次失败后等待 30 秒 |
| 截止时间 | 事件产生超过 24 小时后，平台不再发起投递 |
| 可重试失败 | HTTP 408、425、429、5xx，以及 DNS、连接或读写错误 |
| 不重试失败 | 其他 4xx、3xx、URL 安全校验失败和 HTTPS 证书校验失败 |

只在事件已经可靠入库或业务结果已经持久化后返回 2xx。需要异步处理时，先把事件写入内部队列，再返回 204。返回 3xx 不会让平台跟随重定向。

<span id="query-event-history"></span>
## 查询事件记录

使用事件列表接口核对指定接入点最近 7 天内关联的事件。列表按事件发生时间倒序排列，支持按 `event_id` 或 `event_type` 精确筛选：

```Bash
#!/usr/bin/env bash
curl -sS --fail-with-body \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/events?limit=20&event_type=session.status_idled" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

响应中的 `data` 是冻结事件信封数组。还有更多结果时，`next_page` 返回下一页标识；将该值原样传入 `page` 查询下一页。最后一页返回 `next_page: null`。

需要读取单个事件时，配置列表或接收日志中的事件 ID：

```Bash
export WEBHOOK_EVENT_ID="<WEBHOOK_EVENT_ID>"
```

然后查询事件详情：

```Bash
#!/usr/bin/env bash
curl -sS --fail-with-body \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/events/$WEBHOOK_EVENT_ID" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

事件详情与原回调请求体一致。接口不会重新读取业务资源，不返回投递是否成功、HTTP 尝试次数或每次失败原因，也不支持重放事件。接入点停用后仍可查询；删除接入点后，在 7 天墓碑保留期内仍可查询尚未过期的关联事件。

<span id="operate-endpoint"></span>
## 运维接入点

<span id="pause-and-update"></span>
### 暂停和更新

需要迁移回调地址或进行维护时，先停用接入点：

```Bash
curl -sS --fail-with-body -X POST \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/disable" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

停用会取消尚未开始和等待重试的投递。已经发出的请求仍可能到达，停用期间产生的事件不会在恢复后补发。

更新回调地址、描述或订阅列表。只有停用状态下可以修改回调地址：

```Bash
curl -sS --fail-with-body -X PUT \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID" \
  -H "Authorization: Bearer $ARK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "接收 Session 状态变更通知",
    "events": [
      "session.status_idled",
      "session.status_terminated"
    ]
  }'
```

更新订阅列表会整体替换原列表。移除事件类型会取消尚未开始和等待重试的对应投递；新增事件类型不会补发历史事件。更新后先发送测试事件，再启用接入点：

```Bash
curl -sS --fail-with-body -X POST \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/enable" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

启用成功后，平台清空停用原因和连续失败数，但不会补发停用期间的业务事件。启用接口不会先向回调地址发送探测请求，因此应在启用前发送测试事件并确认接收成功。

<span id="recover-disabled-endpoint"></span>
### 恢复自动停用

一个业务事件经过全部尝试后仍未成功，只计为一次最终投递失败。连续 20 个业务事件最终投递失败时，平台自动停用接入点，并将 `disabled_reason` 设为 `consecutive_failures`。

以下问题也会导致接入点自动停用：

- 回调地址返回 3xx，`disabled_reason` 为 `redirect`。
- 回调地址未通过公网地址安全校验，`disabled_reason` 为 `unsafe_url`。
- HTTPS 证书或主机名校验失败，`disabled_reason` 为 `invalid_certificate`。

恢复时，先查询接入点详情并检查 `disabled_reason`。修复回调服务后，按以下顺序操作：

1. 更新停用状态下的回调地址或订阅列表。
2. 发送测试事件，并确认接收端返回 2xx。
3. 调用启用接口恢复业务事件投递。
4. 通过资源查询接口核对停用期间的状态变化。

<span id="reset-secret"></span>
### 重置签名密钥

怀疑密钥泄露或需要定期轮换时，重置签名密钥。平台不提供新旧密钥并行生效窗口；重置成功后立即使用新密钥签名后续请求。根据业务是否允许短暂停止通知，选择以下方式。

#### 在维护窗口轮换

本指南提供的 Python 接收端一次只读取一个 `WEBHOOK_SIGNING_SECRET`，适合在维护窗口中轮换：

1. 停用 Webhook 接入点。停用会取消尚未开始和等待重试的投递，停用期间产生的事件不会在恢复后补发。
2. 重置签名密钥：

```Bash
curl -sS --fail-with-body -X POST \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID/reset_secret" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

3. 保存响应中的新密钥，更新接收端的 `WEBHOOK_SIGNING_SECRET`，然后重启接收服务。
4. 发送测试事件并确认验签成功。测试事件在接入点停用期间也可以投递。
5. 启用接入点，恢复业务事件投递。
6. 通过 Webhook 事件查询和对应资源查询接口，核对维护窗口内的状态变化。

新密钥仅在本次成功响应中返回。关闭响应前，将新密钥写入密钥管理服务。

#### 持续接收时轮换

需要持续接收业务通知时，不要直接使用“重置密钥后更新环境变量并重启”的顺序。先部署支持受控轮换的接收端，再执行以下步骤：

1. 让接收端进入短时轮换模式：继续使用旧密钥验签；仅在本次受控窗口内，对无法验签的请求返回 HTTP 503，使业务事件进入平台重试流程。
2. 调用重置密钥接口并保存新密钥。
3. 在接收端原子加载新密钥，同时保留旧密钥。过渡期间使用新旧两个密钥依次验签，并继续按事件 ID 去重。
4. 发送测试事件，确认新密钥验签成功。业务事件最多投递 3 次，应在重试次数用尽前完成新密钥加载。
5. 保留旧密钥至少覆盖接收端允许的签名时间窗口；本指南示例为 5 分钟。随后移除旧密钥，并退出轮换模式。

> **警告**
>
> 平台不会同时使用新旧密钥签名。接收端尚未加载新密钥时，新签名请求会使用旧密钥验签失败；接收端移除旧密钥后，已经发出的旧签名请求也会验签失败。只有在明确进入受控轮换模式后，才将暂时无法验签的请求返回 503。正常运行时仍应返回 401，避免攻击请求触发平台重试。

<span id="cleanup"></span>
## 清理资源

确认不再需要事件通知后，使用精确的 `WEBHOOK_ENDPOINT_ID` 删除接入点：

```Bash
curl -sS --fail-with-body -X DELETE \
  "https://ark.cn-beijing.volces.com/api/v3/webhook_endpoints/$WEBHOOK_ENDPOINT_ID" \
  -H "Authorization: Bearer $ARK_API_KEY"
```

删除成功返回 HTTP 204。删除会取消尚未开始和等待重试的投递，并立即清除服务端保存的签名密钥。已经发出的请求仍可能到达。删除后 7 天内重复调用删除接口仍返回 204。

删除接入点后，移除接收服务中的密钥和本地环境变量：

```Bash
unset WEBHOOK_SIGNING_SECRET
unset WEBHOOK_ENDPOINT_ID
unset WEBHOOK_EVENT_ID
```

> **警告**
>
> 删除后的 Webhook 接入点不能恢复。只删除本次创建且记录了精确 ID 的接入点，不要按名称或回调地址模糊匹配批量删除。
