---
title: "在语音序列中使用应用程序数据"
description: "在通话期间向 HTTPS 接收端请求类型化数据，在后续步骤中据此分支，显式处理技术故障，并将私密响应与保留的通话数据分开。"
canonical: "https://bird.com/zh-sg/wendang/guides/voice/sequences/code/webhooks"
---

# 在语音序列中使用应用程序数据

当您的应用程序持有通话所需的数据（如订单状态、团队或预约确认）时，使用 `voice.webhook`。该步骤在 `success` 上返回这些数据，随后的 Branch 步骤据此选择路径。

## 定义响应

将 `config.output_schema` 设置为描述返回数据的对象 schema。该步骤有两个输出：`success` 携带符合该 schema 的数据，`failure` 处理技术问题。如果省略配置，输出为空对象。

输入包含静态 HTTPS `url` 和可选的对象 `data`。数据支持字面量对象、`$literal` 或整个对象的 `$expr`。不支持动态 URL、自定义请求头和凭据。

## 验证请求

Bird 使用您的工作区回调密钥，通过 [Standard Webhooks](https://www.standardwebhooks.com/) 请求头为每个请求（包括续接请求）签名：`webhook-id`、`webhook-timestamp` 和 `webhook-signature`。签名覆盖 ID、时间戳和完整的请求体。密钥轮换后的 24 小时内，`webhook-signature` 为每个有效密钥携带一个 `v1,` 条目，因此任一密钥都能通过验证。通话可能需要约一分钟才开始使用新密钥签名，因此在重叠期结束前请继续接受旧密钥。您的响应不会被签名：Bird 会验证接收端的 TLS 证书，由 HTTPS 连接对返回内容进行身份验证。

请在解析之前，使用 Standard Webhooks 库的 verify 调用基于原始请求体验证这些请求头。SDK 的 `webhooks.unwrap` 用于解析平台事件，不适用于这些请求。请先在 **Webhooks → Direct callbacks** 下轮换密钥，因为初始密钥从不显示。签名表明请求由持有您工作区密钥的一方签署。您的 SMS 回调接收端持有同一密钥，也能签名，因此只在您信任的接收端上保存该密钥。任何能编辑该工作区序列的人都可以选择 URL 和 `data`。请检查 `target_url` 是否为您自己的 URL，以拒绝 Bird 为其他接收端签名的请求，并在接收端中对业务操作进行授权。客户授权从不转发。

即使您的工作区强制使用 mTLS，序列 webhook 请求也不会提供客户端证书。请将接收端配置为接受没有客户端证书的连接，并改为验证已签名的请求。

使用已同步的服务器时钟检查已签名的 `webhook-timestamp`（以秒为单位的 Unix 时间戳）。拒绝早于或晚于当前时间五分钟以上的请求。在执行副作用之前，按 `webhook-id` 去重。尽可能将 ID 和结果与操作一起原子化存储，并对重复请求返回已保存的结果。每次续接都有自己的 ID。如果不同的请求不得重复执行同一操作，请使用单独的业务幂等键。

## 返回数据

接收端返回以下封闭的响应结构：

```json
{
  "schema_version": 1,
  "type": "outcome",
  "output": { "status": "confirmed", "message": "Your appointment is confirmed." }
}
```

`output` 必须符合所配置的 schema。响应返回的是数据，而不是端口、节点 ID 或新命令。续接是在原始 URL 的规范源上的 `{"schema_version":1,"type":"continue","url":"https://example.com/voice/next"}`。

要根据数据进行路由,请将 `success` 连接到 `logic.branch` 步骤,并设置 `steps.decision.output.status == 'confirmed'` 之类的分支条件。在 schema 中将该字段声明为字符串 `enum`,这样接收端只能返回这些值。

请求包含运行、会话和通话的标识,已接受的序列和节点的标识,经过求值的 `data`,以及 `results`。业务输入请放在 `data` 中;直接的私密 Gather 结果可能在 `results` 中出现一次,已完成的录音在可用时带有 ID。这些观察数据不会自动成为表达式字段。

## 读取接收端请求

草稿测试运行会发送如下结构。下面的 ID 和时间戳均为虚构;实际请求使用已接受的运行和当前节点实例:

```json
{
  "schema_version": 1,
  "method": "POST",
  "target_url": "https://example.com/decision",
  "run_id": "vsr_01k5yb7p000000000000000000",
  "sequence": {
    "id": "vsq_01k5yb7p000000000000000000",
    "version_id": null,
    "draft_revision": 1,
    "entry_node_id": "start",
    "mode": "test"
  },
  "session_id": "vcs_01k5yb7p000000000000000000",
  "call": { "id": "vcl_01k5yb7p000000000000000000", "orig": null, "dest": null },
  "node": { "id": "decision", "occurrence_id": "00000000-0000-4000-8000-000000000001" },
  "data": { "appointment_id": "appointment_123" },
  "results": []
}
```

对于已发布的运行，`version_id` 标识所选版本，`draft_revision` 为 null。对于运行了[内联定义](/docs/guides/voice/create-calls)的通话，`id`、`version_id` 和 `draft_revision` 均为 null。`mode` 可以是 `test` 或 `production`。主叫和被叫使用可为 null 的通话方对象。请先验证签名，再信任这些标识字段。

`results` 中的 Gather 观察数据包含 `type: "gather"`、`node_id`、UUID `command_id`、`digits` 和 `reason`。录音观察数据包含 `type: "recording"`、UUID `command_id`、`state`,以及在可用时的 `recording_id`。私密按键数字只应用于目标请求;它们不会成为保留的表达式输出。后续请求保持同一节点实例,已传递过的观察数据会从 `results` 中移除。

## 处理失败

技术故障样例使用 `failure` 端口,值为 `{"code":"webhook_unavailable"}` 或 `{"code":"webhook_invalid_response"}`。已连接的失败端口会进入恢复路径;未连接时运行失败。未连接的 `success` 会使运行正常结束。实时 webhook 请求的截止时间为三秒,且不会自动重试。预览不会发送请求。

三秒截止时间包括读取签名密钥的时间。请将 `failure` 连接到恢复路径,这样当接收端不可用或返回无效响应时,通话仍可继续。超时的请求可能已在接收端产生了副作用。

请将 `data`、通话标识和 Gather 按键数字视为私密数据。限制对接收端和密钥的访问,避免在日志中记录请求体或密钥,并且只在应用需要期间保留来电者输入。不要在 URL 中放入私密数据或凭据:通话追踪会保留完整 URL,包括查询字符串。编写的 URL 和数据以及经过验证的输出会保留在运行历史中;临时的 Gather 按键数字不会记录在步骤输入中。只返回序列需要的数据。

在 `steps.decision.output` 读取返回的数据;在 `steps.decision.error.code` 读取技术错误。如果两条路径汇合,请先用 `has(steps.decision.output)` 或 `has(steps.decision.error)` 检查包装对象,再使用它。接收端返回 HTTP 200 并不代表结果有效,其响应体还必须符合约定。

## Related resources

- [Voice sequences](/docs/guides/voice/sequence-builder) (docs)
