通话事件与录音 API
说明通话事件 Webhook 载荷、录音事件与录音下载地址重新签发接口
简述
通过 通话集成 → 事件订阅 配置规则后,OpenDesk 会把命中的通话事件以 Webhook 形式推送到客户服务器。若事件中包含录音临时下载地址,地址过期后,可使用 API Key 调用开放接口重新签发下载地址。
本文面向客户技术团队,说明事件接收、去重、常见事件与录音补拉方式。
接入前准备
| 准备项 | 说明 |
|---|---|
| 事件订阅 Endpoint | 在 全局设置 → 事件订阅 中创建,保存 Webhook Secret |
| 通话事件规则 | 在 呼叫中心 → 通话集成 → 事件订阅 中配置方向、来源与事件类型 |
| API Key | 在 全局设置 → API Key 中创建,用于录音地址过期后重新签发 |
Webhook 请求
OpenDesk 以 POST 方式向订阅 Endpoint 推送事件。接收方应尽快返回 2xx,耗时处理建议异步执行。
常见请求头:
Content-Type: application/json
User-Agent: OpenDesk-Webhook/1.0
Authorization: Bearer <Webhook Secret>
X-OpenDesk-Delivery: <本次投递标识>
建议接收方:
- 使用固定时间比较方式校验
Authorization中的 Webhook Secret。 - 使用正文中的
id去重;同一事件重试时id不变。 - 使用
subject或data.call.public_id把同一通电话的事件归并。 - 使用
data.sequence判断同一通电话内事件发生顺序。
Endpoint 返回 200~299 视为投递成功。超时、连接失败或返回其他状态码时,OpenDesk 会自动重试;测试请求不重试。
事件正文结构
事件正文遵循 CloudEvents 风格,示例:
{
"specversion": "1.0",
"id": "evt_稳定事件标识",
"source": "opendesk://call-center",
"type": "call.ended",
"subject": "call/11111111-2222-3333-4444-555555555555",
"time": "2026-10-23T04:34:38.912000+00:00",
"datacontenttype": "application/json",
"dataschema": "opendesk://schemas/events/call.ended/v1",
"apiversion": "v1",
"deliveryid": "本次投递标识",
"test": false,
"data": {
"sequence": 10,
"call": {},
"event": {}
}
}
事件信封字段
| 字段 | 说明 |
|---|---|
specversion | 固定为 1.0 |
id | 稳定事件标识,用于去重;同一事件重试时保持不变 |
source | 通话事件固定为 opendesk://call-center |
type | 事件类型,如 call.created、call.ended |
subject | 通话公开标识,格式为 call/{call_public_id} |
time | 事件事实发生时间,不是客户服务器收到时间 |
datacontenttype | 固定为 application/json |
dataschema | 事件 Schema 标识,如 opendesk://schemas/events/call.ended/v1 |
apiversion | 固定为 v1 |
deliveryid | 本次 HTTP 投递标识,与请求头 X-OpenDesk-Delivery 相同;不要用它做业务去重 |
test | 是否为测试请求;正式通话事件为 false |
data.sequence | 同一通电话内的事件序号;序号可能跳号,跳号不代表丢事件 |
data.call | 通话摘要,每条通话事件都会携带 |
data.event | 当前事件自己的字段 |
后台 发送测试请求 的事件类型为 opendesk.webhook.test,用于验证 Endpoint 连通性,不应按通话事件解析。
常见事件类型
| 事件 | 何时出现 | 一通电话中可能出现次数 |
|---|---|---|
call.created | 通话记录已创建,外呼刚开始 | 一次 |
call.customer.connected | 客户已接通;外呼时表示客户已接听 | 最多一次 |
call.queue.entered | 通话进入员工或员工组队列 | 可多次 |
call.agent.connected | 坐席开始服务客户 | 转接后可多次 |
call.agent.disconnected | 一段人工服务结束 | 可多次 |
call.transfer.completed | 转给坐席、队列或外部号码成功 | 可多次 |
call.ended | 通话结算完成,结果与时长已落库 | 一次 |
call.recording.completed | 一段录音已完成或部分完成 | 每段录音一次 |
call.recording.failed | 一段录音失败 | 每段录音一次 |
call.satisfaction.completed | 满意度调查完成 | 每次调查一次 |
没有发生的阶段不会推送。例如客户未接通的外呼通常不会有坐席接入、人工段录音或满意度事件。
通话摘要 data.call
不同事件会携带当前已发生的通话摘要字段。尚未发生或无法关联的信息通常为 null。
每条通话事件都会携带
| 字段 | 说明 |
|---|---|
public_id | 通话公开标识,与 subject 中 / 后面的值相同 |
direction | 通话方向:inbound 呼入,outbound 外呼 |
source | 通话来源,如 embedded_call、agent_dial、auto_outbound、manual_outbound_task、personal_auto_outbound |
lifecycle_status | 通话生命周期状态,常见为 active、ended |
customer_number | 客户号码 |
service_number | 服务号码 / 外显号码 |
user | 已关联用户时为 {"public_id":"usr_..."},否则为 null |
initiator_agent | 发起坐席,结构为 {"username":"登录名","name":"显示名"} |
last_serving_agent | 最近服务坐席,结构同 initiator_agent |
last_queue | 最近进入的队列,未进过队列时为 null |
started_at | 通话开始时间 |
customer_connected_at | 客户接通时间 |
ended_at | 通话结束时间 |
external_context | 嵌入式通话拨号时传入的外部上下文,便于客户系统关联订单、工单或会员 |
仅 call.ended 携带
| 字段 | 说明 |
|---|---|
finalization_status | 结算状态,常见为 completed |
call_result | 通话结果,如 agent_answered、customer_busy、customer_no_answer、failed |
hangup_party | 挂断方,如 customer、agent、system、carrier、unknown |
hangup_reason_code | 挂断原因代码,如 agent_hangup、customer_busy、customer_no_answer、provider_rejected |
actual_reception_type | 实际接待类型,如 agent、bot、bot_agent、ivr;未接通时通常为 null |
total_duration_ms | 从开始到结束的总时长,单位毫秒 |
customer_ring_duration_ms | 客户侧振铃时长,单位毫秒 |
agent_wait_duration_ms | 等待坐席时长,单位毫秒 |
customer_agent_talk_duration_ms | 客户与坐席实际通话时长,单位毫秒 |
recording_status | 整通电话的录音汇总状态,如 none、recording、completed、partial、failed |
custom_business_info | 嵌入式通话拨号时传入的业务文本;只在 call.ended 返回 |
常见 call_result:
| 值 | 含义 |
|---|---|
agent_answered | 坐席曾经接起,形成了人工服务 |
customer_busy | 客户忙线或对方按拒接 |
customer_no_answer | 客户未接听,振铃超时 |
customer_unreachable | 客户不可达或关机 |
invalid_number | 号码无效 |
agent_no_answer | 已经进队列或振铃给坐席,但坐席未接 |
bot_completed | 机器人服务完成 |
canceled | 坐席或系统在接通前取消 |
failed | 其他失败,包括线路拒绝、发起失败、媒体失败等 |
客户拒接时通常表现为 call_result=customer_busy、hangup_reason_code=customer_busy、hangup_party=carrier。线路直接拒绝时通常表现为 call_result=failed、hangup_reason_code=provider_rejected。
当前事件内容 data.event
data.event 表示当前这一条事件自己的字段。通话最终结果、总时长和挂断原因仍以 call.ended 的 data.call 为准。
| 事件 | data.event 主要字段 |
|---|---|
call.created | created_at:通话记录创建时间 |
call.customer.connected | connected_at:客户接通时间 |
call.queue.entered | queue.type、queue.name:进入的队列;entered_at:进入时间 |
call.agent.connected | agent:接入坐席;connected_at:这一段人工服务开始时间 |
call.agent.disconnected | agent:离开坐席;disconnected_at:这一段结束时间;duration_ms:这一段人工服务时长 |
call.transfer.completed | transfer_type:agent、queue 或 external;from_agent:转出坐席;target:转接目标;completed_at:转接完成时间 |
call.ended | ended_at:通话结束时间 |
call.recording.completed / call.recording.failed | recording:录音对象,见下一节 |
call.satisfaction.completed | status:满意度状态;result:首题答案;trigger_source:触发来源;completed_at:完成时间 |
录音事件
录音事件中的 data.event.recording 表示一段录音。示例:
{
"recording": {
"public_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"phase": "human",
"status": "completed",
"started_at": "2026-10-23T04:34:31.650000+00:00",
"ended_at": "2026-10-23T04:34:38.912000+00:00",
"duration_ms": 7340,
"download_url": "https://临时下载地址",
"download_url_expires_at": "2026-10-23T05:34:39.260000+00:00",
"failure_reason": null
}
}
| 字段 | 说明 |
|---|---|
public_id | 录音公开标识,地址过期后用于重新签发 |
phase | 录音阶段:bot 机器人段、human 人工段、other 其他阶段 |
status | 录音状态:completed 完整、partial 部分完成、failed 失败 |
started_at / ended_at | 录音开始 / 结束时间 |
duration_ms | 录音时长,单位毫秒 |
download_url | 临时下载地址,completed 和 partial 时返回 |
download_url_expires_at | 下载地址失效时间 |
failure_reason | 失败原因;失败录音没有下载地址 |
收到下载地址后建议尽快转存到客户自己的存储。请勿将临时地址暴露到前端页面或长期日志中。
回调案例
案例一:客户接听,坐席挂断
坐席从嵌入式软电话拨出,客户接听后通话 42 秒,最后坐席挂断。规则如果勾选了完整过程,通常会依次收到 call.created、call.customer.connected、call.agent.connected、call.agent.disconnected、call.ended;开启录音时,结束后还可能收到 call.recording.completed。
这类通话的 call.ended 关键字段通常如下:
{
"type": "call.ended",
"subject": "call/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"data": {
"sequence": 8,
"call": {
"public_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"direction": "outbound",
"source": "embedded_call",
"customer_number": "13800138000",
"service_number": "02158000000",
"initiator_agent": { "username": "agent01", "name": "张敏" },
"last_serving_agent": { "username": "agent01", "name": "张敏" },
"customer_connected_at": "2026-10-26T03:25:09.880000+00:00",
"ended_at": "2026-10-26T03:25:52.184000+00:00",
"external_context": { "record_type": "ticket", "record_id": "T-1001" },
"finalization_status": "completed",
"call_result": "agent_answered",
"hangup_party": "agent",
"hangup_reason_code": "agent_hangup",
"total_duration_ms": 48082,
"customer_agent_talk_duration_ms": 42304,
"recording_status": "none",
"custom_business_info": "订单 SO-10086"
},
"event": {
"ended_at": "2026-10-26T03:25:52.184000+00:00"
}
}
}
recording_status 在结束事件中为 none,只表示结束这一刻录音还没有完成;后续如果收到 call.recording.completed,应按同一个 subject 归到这通电话。
案例二:客户拒接或未接
客户拒接、忙线或一直未接时,通常只会收到 call.created 和 call.ended。不会有 call.customer.connected、坐席接入、人工录音或满意度事件。
{
"type": "call.ended",
"subject": "call/bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"data": {
"sequence": 7,
"call": {
"public_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"direction": "outbound",
"source": "embedded_call",
"customer_number": "13800138001",
"customer_connected_at": null,
"last_serving_agent": null,
"finalization_status": "completed",
"call_result": "customer_busy",
"hangup_party": "carrier",
"hangup_reason_code": "customer_busy",
"total_duration_ms": 4903,
"customer_ring_duration_ms": 4903,
"customer_agent_talk_duration_ms": 0,
"recording_status": "none"
}
}
}
如果客户一直未接,常见结果为 call_result=customer_no_answer、hangup_reason_code=customer_no_answer。如果是线路直接拒绝,常见结果为 call_result=failed、hangup_reason_code=provider_rejected。
案例三:录音完成并转存
录音完成后会收到 call.recording.completed。一通电话可能有多段录音,每段录音都会有自己的 recording.public_id。
{
"type": "call.recording.completed",
"subject": "call/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"data": {
"sequence": 9,
"call": {
"public_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"source": "embedded_call",
"recording_status": "completed"
},
"event": {
"recording": {
"public_id": "rrrrrrrr-1111-2222-3333-444444444444",
"phase": "human",
"status": "completed",
"duration_ms": 42304,
"download_url": "https://临时下载地址",
"download_url_expires_at": "2026-10-26T04:26:10.000000+00:00",
"failure_reason": null
}
}
}
}
客户服务器收到后应尽快下载并转存。若临时地址已过期,可使用 recording.public_id 重新签发下载地址。
重新签发录音下载地址
使用 API Key 重新签发地址。接口只允许查询当前 Key 所属租户的录音。
按通话查询录音列表
GET {OpenDesk地址}/api/v1/open/call-center/calls/{call_public_id}/recordings
Authorization: Bearer sk-odk-您的密钥
返回该通电话的录音列表,并为可下载录音重新签发约 1 小时有效的下载地址。
按录音重新签发单段地址
GET {OpenDesk地址}/api/v1/open/call-center/recordings/{recording_public_id}/download-url
Authorization: Bearer sk-odk-您的密钥
失败录音或没有文件的录音不会返回下载地址。
错误与限制
| 状态 | 常见原因 |
|---|---|
401 | API Key 缺失、无效或已停用 |
404 | 通话或录音不存在,或不属于当前租户 |
409 | 录音失败或没有可下载文件 |
429 | 重新签发过于频繁 |
单个 API Key 的录音地址签发有频率限制。批量补拉时建议控制并发与重试节奏。
排查建议
- 回调没到:先确认 事件订阅 测试请求成功,再确认 通话集成 → 事件订阅 规则已启用且匹配方向、来源和事件类型。
- 事件不完整:没有发生的通话阶段不会推送;例如客户未接通时,不会有坐席接入事件。
- 录音地址过期:使用本文的录音开放接口重新签发。
- 业务对不上:使用
subject中的通话公开标识,在 服务运营 → 通话记录 中搜索核对。
