---
title: "创建语音呼叫"
description: "通过 API 使用已发布的 Voice 序列或内联定义发起外呼，查看其结果，并了解 CLI 和 MCP 预览。"
canonical: "https://bird.com/zh-sg/wendang/guides/voice/create-calls"
---

# 创建语音呼叫

> **Warning:** Voice sequences are in preview. Steps, entry data, and the sequence definition format can change in ways that break existing sequences and API requests.

使用 [`POST /v1/voice/calls`](/docs/api/reference/create-voice-call) 拨打电话号码，并在接听方应答后启动已发布的序列，或随请求发送的序列定义。该请求会发起真实通话，适用正常通话费用。

**CLI 和 MCP 预览版：** 这些接口需要兼容的客户端。检查你的 MCP 服务器是否提供 `voice_calls_create`，或运行 `bird voice --help` 列出你的 CLI 中的命令。使用 `bird update --check` 检查是否有更新版本，并参阅 [CLI 安装说明](/docs/cli)。如果工具或命令不可用，请使用 HTTP API 或 SDK。

在预览中，CLI 和 MCP 准备相同的呼叫，供用户在浏览器中审核并执行。用户执行请求后，代理会收到已记录的接受结果。

## 开始之前

在同一工作区中准备以下资源：

- 要运行的序列：在仪表盘中构建的已启用、未归档的[具有活跃发布版本的序列](/docs/guides/voice/sequences/publishing)，或随请求发送的完整定义。
- 用于 HTTP 请求的 [API 密钥](/docs/api/authentication)，或用于 CLI 和 MCP 的 OAuth 登录，需同时具有 `voice_management:write` 和 `voice:write` 权限。
- 工作区可以展示的主叫号码，例如已拥有或已验证的[来电显示号码](/docs/guides/voice/caller-ids)，以及已启用的[目的地国家/地区](/docs/guides/voice/destinations)。优先使用与收件人同一国家/地区的号码：部分移动网络会拒绝显示外国主叫号码的来电。
- 首次呼叫时由您控制的接听方。

`sequence` 对象只接受 `sequence.id` 或 `sequence.definition` 中的一个。两者都需要 `entry_node_id`（即呼叫开始的**Start call**步骤）和 `trigger_data`（一个与该入口数据模式匹配的对象）。当入口接受空数据时，使用 `{}`。[Create Call 参考文档](/docs/api/reference/create-voice-call)记录了请求限制和可选的振铃超时。

## 呼叫在仪表盘中构建的序列

使用 `sequence.id` 运行您在仪表盘中[构建](/docs/guides/voice/sequences/editor)并[发布](/docs/guides/voice/sequences/publishing)的序列。Create Call 始终运行其活跃的发布版本。

1. 在[**序列**](https://bird.com/dashboard/w/voice/sequences)中打开该序列。
2. 在 **How to run this sequence** 下，选择 **Create a call** 旁的 **More details**。
3. 如果序列有多个入口，选择要启动的 **Call trigger**。
4. 复制请求。其中包含序列 ID 和入口 ID。

将请求体保存为 `call.json`，用您自己的号码替换示例号码：

```json
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "id": "YOUR_SEQUENCE_ID",
    "entry_node_id": "YOUR_ENTRY_NODE_ID",
    "trigger_data": {}
  }
}
```

使用入口声明的字段填充 `trigger_data`，包括嵌套对象和数组。Create Call 不接受历史版本、草稿或测试模式。若要测试已保存的草稿，请使用仪表盘的[发起呼叫](/docs/guides/voice/sequences/testing)控件。

## 运行内联序列

使用 `sequence.definition` 随呼叫发送完整的序列。Bird 仅运行一次且不保存，因此呼叫没有序列 ID。内联定义适用于应用程序按每次呼叫动态生成的流程。

内联定义必须通过与发布序列相同的检查。其运行最多可执行 16 条命令，包括收集提示；需要第 17 条命令的呼叫将以 `call_limit` 跟踪错误结束。[节点参考](/docs/guides/voice/sequences/node-reference)列出了可用的每个步骤及其字段和结果。要从一个可用的流程开始，请在控制面板中构建并发布它，打开序列，然后在 **Published version** 下的 **Definition JSON** 上选择 **Copy JSON**。

此 `call.json` 要求接听方按 1 查询营业时间，或按 2 获取路线指引。静默和其他任何按键会以各自的退出原因结束运行：

```json
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "entry_node_id": "start",
    "trigger_data": {},
    "definition": {
      "schema_version": 1,
      "expression_environment": "bird.cel.v1",
      "nodes": [
        {
          "id": "start",
          "type": "trigger.start_call",
          "type_version": 1,
          "config": {},
          "input": {},
          "connections": { "event": { "node_id": "menu", "port": "input" } }
        },
        {
          "id": "menu",
          "type": "voice.gather",
          "type_version": 1,
          "config": {},
          "input": {
            "prompts": [
              {
                "type": "say",
                "text": "Press 1 for opening hours, or 2 for directions.",
                "language": "en"
              }
            ],
            "max_digits": 1,
            "timeout_seconds": 5,
            "inter_digit_timeout_seconds": 2,
            "finish_on_key": "#",
            "private": false,
            "matches": { "hours": "1", "directions": "2" }
          },
          "connections": {
            "hours": { "node_id": "hours", "port": "input" },
            "directions": { "node_id": "directions", "port": "input" },
            "timeout": { "node_id": "no_input", "port": "input" },
            "fallback": { "node_id": "other_input", "port": "input" }
          }
        },
        {
          "id": "hours",
          "type": "voice.say",
          "type_version": 1,
          "config": {},
          "input": { "text": "We are open Monday through Friday, nine to five.", "language": "en" },
          "connections": { "next": { "node_id": "done", "port": "input" } }
        },
        {
          "id": "directions",
          "type": "voice.say",
          "type_version": 1,
          "config": {},
          "input": {
            "text": "Find directions on your appointment confirmation.",
            "language": "en"
          },
          "connections": { "next": { "node_id": "done", "port": "input" } }
        },
        {
          "id": "done",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "information_played" },
          "input": { "output": {} }
        },
        {
          "id": "no_input",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "no_input" },
          "input": { "output": {} }
        },
        {
          "id": "other_input",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "unrecognized_input" },
          "input": { "output": {} }
        }
      ]
    }
  }
}
```

每个节点的 `connections` 将结果映射到下一个节点。菜单的 `matches` 命名其选项，`connections` 使用相同的名称以及内置的 `timeout` 和 `fallback` 结果。

### 个性化内联序列

在入口的 `data_schema` 中声明要发送的字段，然后在 `$expr` 值中以 `trigger.data.<field>` 的形式读取它们。此呼叫按姓名问候接听方，并确认或标记其预约：

```json
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "entry_node_id": "start",
    "trigger_data": {
      "appointment_id": "apt_2048",
      "customer_name": "Sam",
      "appointment_time": "Tuesday at 10 AM"
    },
    "definition": {
      "schema_version": 1,
      "expression_environment": "bird.cel.v1",
      "nodes": [
        {
          "id": "start",
          "type": "trigger.start_call",
          "type_version": 1,
          "config": {
            "data_schema": {
              "type": "object",
              "additionalProperties": false,
              "required": ["appointment_id", "customer_name", "appointment_time"],
              "properties": {
                "appointment_id": { "type": "string", "minLength": 1 },
                "customer_name": { "type": "string", "minLength": 1 },
                "appointment_time": { "type": "string", "minLength": 1 }
              }
            }
          },
          "input": {},
          "connections": { "event": { "node_id": "confirm_menu", "port": "input" } }
        },
        {
          "id": "confirm_menu",
          "type": "voice.gather",
          "type_version": 1,
          "config": {},
          "input": {
            "prompts": [
              {
                "type": "say",
                "text": {
                  "$template": [
                    "Hello ",
                    { "$expr": "trigger.data.customer_name" },
                    ". Press 1 to confirm your appointment on ",
                    { "$expr": "trigger.data.appointment_time" },
                    ", or 2 if you need help."
                  ]
                },
                "language": "en"
              }
            ],
            "max_digits": 1,
            "timeout_seconds": 7,
            "inter_digit_timeout_seconds": 2,
            "finish_on_key": null,
            "private": false,
            "matches": { "confirm": "1", "help": "2" }
          },
          "connections": {
            "confirm": { "node_id": "confirmed", "port": "input" },
            "help": { "node_id": "help_requested", "port": "input" },
            "timeout": { "node_id": "no_response", "port": "input" },
            "fallback": { "node_id": "no_response", "port": "input" }
          }
        },
        {
          "id": "confirmed",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "appointment_confirmed" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        },
        {
          "id": "help_requested",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "appointment_help_requested" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        },
        {
          "id": "no_response",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "failed", "reason": "appointment_no_response" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        }
      ]
    }
  }
}
```

每个出口的 `status` 和 `reason` 区分确认、请求帮助和无响应。不匹配 `data_schema` 的 `trigger_data` 对象会验证失败，错误位置位于 `/sequence/trigger_data` 下，且不会发起呼叫。

## 发送请求

为您保存的 `call.json` 选择以下一种请求方式。

为此次预期呼叫选择一个新的唯一幂等键。保留该键和原始输入，以便重试或恢复确认。当您打算发起另一个新呼叫时，请先解决原始呼叫的未知结果，然后选择另一个键。

对于 shell 示例，在发送前将 `IDEMPOTENCY_KEY` 设置为您选择的值。如果该值未设置或为空，示例将停止；重复执行时不会生成新键。

对于 HTTP，将 `BIRD_API_KEY` 设置为您的密钥，将 `BIRD_API_URL` 设置为其[区域基础 URL](/docs/api/regions)（例如 `eu1` 密钥使用 `https://eu1.platform.bird.com`），然后发送请求：

```bash
curl --request POST "$BIRD_API_URL/v1/voice/calls" \
  --header "Authorization: Bearer $BIRD_API_KEY" \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: ${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}" \
  --data-binary @call.json
```

对于 [CLI](/docs/cli) 预览，使用两项写入权限登录，并使用相同的文件准备呼叫：

```bash
bird auth login --scope voice_management:write --scope voice:write
bird voice calls create --body-file call.json \
  --idempotency-key "${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}"
```

命令会打印浏览器审核链接并等待完成。打开链接，检查主叫方、接听方、序列和入口数据，然后自行执行请求。准备请求不会发起呼叫。添加 `--dry-run` 可查看 CLI 请求而不创建确认。如果确认过期、被取消或结束时没有记录结果，命令将以 `5` 退出；在发起新请求之前，请遵循[重试指引](#处理错误和重试)。

对于 [MCP](/docs/ai/mcp-server) 预览，使用 `call.json` 中的字段和必需的 `idempotency_key` 调用 `voice_calls_create`。将 `<unique-key-for-this-request-reuse-for-retries>` 替换为您选择的键：

```json
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "id": "YOUR_SEQUENCE_ID",
    "entry_node_id": "YOUR_ENTRY_NODE_ID",
    "trigger_data": {}
  },
  "idempotency_key": "<unique-key-for-this-request-reuse-for-retries>"
}
```

按照返回的浏览器审核链接执行请求。在动态 MCP 端点上，通过 `execute` 选择 `voice_calls_create`。OAuth 授权授予工作区权限；浏览器审核执行的是此次具体呼叫。

`202 Accepted` 响应确认已受理。序列在接听方接听后开始。接受后发布更新版本不会更改此次呼叫选定的定义或入口数据。

## 读取接受结果

保留响应中的 `id`、`initial_leg_id` 和 `sequence.run_id` 用于关联。顶层的 `id` 标识跨呼叫段的呼叫；`initial_leg_id` 标识其第一个呼叫段。

响应是不可变的接受快照。其 `started_at` 和 `ended_at` 为 `null`，观测布尔值为 `false`，`parties` 为空。这些字段描述的是接受状态，因此重放请求时它们不会变成实时状态报告。

打开序列的[**Runs**](/docs/guides/voice/sequences/runs)以查看进度。内联呼叫不属于任何序列，因此请改为跟踪其呼叫段；如果在呼叫段注册之前失败，目前没有方法可以查看它。呼叫段记录存在后，可使用 [`GET /v1/voice/legs/{leg_id}`](/docs/api/reference/get-voice-leg) 配合 `initial_leg_id`，或查看[通话记录](/docs/guides/voice/call-log)。读取呼叫段需要 `voice:read` 权限，`voice:write` 包含该权限。

`status` 为 `rejected` 的呼叫段表示在接通前被拒绝，因此其序列未运行。如果没有拒绝原因，则说明远端拒绝了来电；请参阅[被拒绝的呼叫](/docs/guides/voice/call-log#rejected-calls)。

使用 CLI，`bird voice calls get YOUR_CALL_ID` 显示通话及其各方。`bird voice legs trace YOUR_LEG_ID` 显示已执行的步骤、收集到的按键（除非 gather 设为私密），以及退出的状态和原因。跟踪记录在通话结束后不久即可获取；在此之前该命令会返回错误，请重试。被拒绝的通话分支的序列从未执行，因此可能没有跟踪记录。

接受不保证拨号、接听或可读的通话记录。在注册之前失败的呼叫可能出现在运行历史中但没有呼叫段记录。接听的呼叫也不意味着序列完成了其预期的业务任务。

## 处理错误和重试

对于直接 HTTP 请求，幂等性是可选的。没有键时，每个请求都会接受一个新的呼叫尝试。SDK 会为自动重试生成键。要在多次独立呼叫或直接 HTTP 请求之间保护重试，请在首次尝试时提供键，并使用完全相同的原始请求字节复用该键。参见[幂等性指南](/docs/guides/idempotency)。

三小时内的相同重放返回原始接受结果和 `Idempotency-Replay: true`。使用该键但更改了输入会返回 `409`。重放时会重新检查授权和主叫号码权限。重放窗口过后，该键可以受理新呼叫，因此在重新提交之前请先核对原始结果。

对于 CLI 和 MCP，在首次尝试时提供幂等键。使用相同的请求、键和相同的 OAuth 授权重复提交时，只要确认记录仍然保留，就会返回已有的确认。保留原始输入和确认 ID。更改键可能会准备另一个计费呼叫。

如果 CLI 命令被中断，请使用原始输入、键和返回的确认 ID 恢复：

```bash
bird voice calls create --body-file call.json \
  --idempotency-key "${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}" \
  --confirmation-id YOUR_CONFIRMATION_ID
```

对于 MCP，重复原始工具参数（包括相同的 `idempotency_key`）以及返回的请求状态。该状态标识确认，不会替代这些参数。或者，使用返回的 ID 放入 `bird/confirmation-id` 请求元数据中，重复相同的工具和参数。对于动态 MCP，重复相同的 `execute` 请求并将该元数据附加到请求上。恢复操作会读取原始确认并在完成后返回其记录的结果。

Create Call 确认在 90 分钟后过期。没有结果的 `pending` 或 `expired` 确认并不能证明呼叫未曾执行。在决定发起另一个呼叫之前，请检查原始序列运行记录和任何呼叫段记录；不要通过创建新键来解决未知结果。带有 `202` 结果的已完成确认证明已被接受，因此请继续执行[接受检查](#读取接受结果)以确定呼叫的最终结果。

| 症状           | 检查项                                                                                            |
| -------------- | ------------------------------------------------------------------------------------------------- |
| `400` 或 `422` | 阅读错误的字段详情。检查号码格式、主叫号码资格和所有权、序列和入口 ID、显式数据对象以及请求限制。 |
| `401` 或 `403` | 检查您的 API 密钥或 OAuth 登录、两项必需的写入权限以及工作区访问权限。                            |
| `404`          | 检查序列是否属于所选工作区，以及区域服务是否支持呼叫创建。                                        |
| `409`          | 检查是否存在更改了请求体的重试或活跃生产序列不可用的情况。根据需要发布或恢复，然后再发起新呼叫。  |
| `429`          | 遵循响应中的重试指引。如果您提供了键，请为同一呼叫尝试复用该键。                                  |
| 已接受但失败   | 在决定发起另一个呼叫之前，检查运行记录和任何呼叫段记录中的目的地、限制、余额或运行时故障。        |

## 后续步骤

- [Create Call API 参考文档](/docs/api/reference/create-voice-call)。
- [定义序列入口数据](/docs/guides/voice/sequences/data)。
- [查看序列运行记录](/docs/guides/voice/sequences/runs)。
- [接收呼叫事件](/docs/guides/voice/events)。

## Related resources

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