Sign inGet Started

创建语音呼叫

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 拨打电话号码,并在接听方应答后启动已发布的序列,或随请求发送的序列定义。该请求会发起真实通话,适用正常通话费用。

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

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

开始之前

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

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

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

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

使用 sequence.id 运行您在仪表盘中构建并发布的序列。Create Call 始终运行其活跃的发布版本。

  1. 在序列中打开该序列。
  2. 在 How to run this sequence 下,选择 Create a call 旁的 More details。
  3. 如果序列有多个入口,选择要启动的 Call trigger。
  4. 复制请求。其中包含序列 ID 和入口 ID。

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

代码示例
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "id": "YOUR_SEQUENCE_ID",
    "entry_node_id": "YOUR_ENTRY_NODE_ID",
    "trigger_data": {}
  }
}

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

运行内联序列

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

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

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

代码示例
{
  "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> 的形式读取它们。此呼叫按姓名问候接听方,并确认或标记其预约:

代码示例
{
  "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(例如 eu1 密钥使用 https://eu1.platform.bird.com),然后发送请求:

代码示例
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 预览,使用两项写入权限登录,并使用相同的文件准备呼叫:

代码示例
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 预览,使用 call.json 中的字段和必需的 idempotency_key 调用 voice_calls_create。将 <unique-key-for-this-request-reuse-for-retries> 替换为您选择的键:

代码示例
{
  "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以查看进度。内联呼叫不属于任何序列,因此请改为跟踪其呼叫段;如果在呼叫段注册之前失败,目前没有方法可以查看它。呼叫段记录存在后,可使用 GET /v1/voice/legs/{leg_id} 配合 initial_leg_id,或查看通话记录。读取呼叫段需要 voice:read 权限,voice:write 包含该权限。

status 为 rejected 的呼叫段表示在接通前被拒绝,因此其序列未运行。如果没有拒绝原因,则说明远端拒绝了来电;请参阅被拒绝的呼叫。

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

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

处理错误和重试

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

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

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

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

代码示例
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遵循响应中的重试指引。如果您提供了键,请为同一呼叫尝试复用该键。
已接受但失败在决定发起另一个呼叫之前,检查运行记录和任何呼叫段记录中的目的地、限制、余额或运行时故障。

后续步骤

继续查看此主题的文档、指南和示例。