创建语音呼叫
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 始终运行其活跃的发布版本。
- 在序列中打开该序列。
- 在 How to run this sequence 下,选择 Create a call 旁的 More details。
- 如果序列有多个入口,选择要启动的 Call trigger。
- 复制请求。其中包含序列 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 | 遵循响应中的重试指引。如果您提供了键,请为同一呼叫尝试复用该键。 |
| 已接受但失败 | 在决定发起另一个呼叫之前,检查运行记录和任何呼叫段记录中的目的地、限制、余额或运行时故障。 |
后续步骤
相关资源
继续查看此主题的文档、指南和示例。