Webhook 参考
事半AI 平台向外部系统推送事件的 Webhook 说明——话单回调与项目结算事件。
事半AI 平台通过 Webhook 主动向接入方推送事件。目前支持的事件如下:
| 事件 | 触发时机 | 说明 |
|---|---|---|
| 话单回调 | 一通电话结束后 | 推送该通通话的话单、意向结果与完整对话记录,可选 WT / VS 两种格式 |
| 项目结算事件 | 一通电话结算后 | 轻量通知:接通与否、时长、意向,可通过 API 自助配置,见下文 |
两者相互独立:话单回调内容完整,需要联系平台开通并配置签名参数;项目结算事件字段较少,但可以自己用 API 按项目开关,适合只需要"打完了、接没接、有没有意向"的场景。
话单回调(接入方实现)
接通的通话结束后,系统主动向接入方推送话单(含通话记录、意向结果、对话日志)。未接通的电话默认不推送,开启「推送未接通话单」后也会推送,见下方注意事项。
推送格式
话单回调有两种格式,在数字员工编辑 → 意向抽取 → 话单推送 的「推送格式」中选择。两种格式字段不同,签名方式也不同,接入前请先确认使用哪一种:
| 格式 | 特点 | 说明 |
|---|---|---|
| WT | 字段固定、拍平;用 appId + appSecret 签名,签名头为 X-Sign | 本节下方的 Header / Body / 签名规则均为 WT 格式 |
| VS | 结构化 JSON(意向、通话时间分组),全部抽取字段一并推送;可选认证方式、自定义请求头、签名 | 见下文 VS 格式 |
两种格式都支持以下筛选(在话单推送配置中开启):
- 只推接通话单:未接通的通话不推送
- 只推有意向话单:只推送 AI 判定为有意向的通话
- 推送未接通话单:未接通的电话也推送一条话单(默认关闭;开着上面两项时仍不推未接通)
接入方需提供
| 配置项 | 说明 |
|---|---|
| 推送地址(url) | 接收话单的 POST 接口地址(HTTP / HTTPS 均可,建议 HTTPS) |
| 应用 ID(appId) | 标识接口调用方,也可用于区分业务 |
| 应用密钥(appSecret) | 用于签名验证 |
| 加密方法 | md5 或 sha256 |
请求 Header
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Content-Type | string | ✓ | application/json |
| X-App-Id | string | ✓ | 应用 ID |
| X-Timestamp | int64 | ✓ | 当前时间戳(秒) |
| X-Nonce | string | ✓ | 随机字符串(防重放) |
| X-Sign | string | ✓ | 签名值 |
| X-Sign-Method | string | ✓ | md5 或 sha256 |
接入方要求额外的请求头时(例如要求头名叫小写的
appid),可在话单推送配置里填写「自定义请求头」,会和上表的头一起发出;上表的签名头不会被同名的自定义头覆盖。
请求 Body
{
"contextId": "string", // 三方业务标识——原样回传推送线索时传入的 contextId(如订单号),未传则为空字符串
"callId": "string", // 话单唯一标识(以此字段做幂等)
"phone": "string", // 被叫手机号
"caller": "string", // 线路主叫账号,见下方说明(不一定是电话号码格式)
"position": "string", // 项目名称
"callMakeTime": 1234567890, // 呼叫发起时间(秒级时间戳)
"callRingTime": 1234567890, // 振铃时间(秒级时间戳)
"callAnswerTime": 1234567890, // 接通时间(秒级时间戳,未接通则为 0)
"callHangupTime": 1234567890, // 挂机时间(秒级时间戳)
"callDuration": 5, // 通话时长(分钟,向上取整,不满1分钟按1分钟算;精确时长请用 callDurationSeconds)
"callDurationSeconds": 300, // 通话时长(秒),精确值
"recordUrl": "string", // 通话录音下载地址(有效期 7 天)
"summary": "string", // AI 生成的通话摘要
"intentionStatus": 2, // 意向状态:2=有意向 1=无意向 0=无结果(见下方「意向字段取值」)
"intentionRank": "是", // 是否有意向:只有 "是" / "否" 两个值,不是 A/B/C/D 分级(分级在 extractData 里)
"intentionTag": { // 固定 4 个字段
"summary": "string", // 同顶层 summary
"wechat_id": "", // 加微信场景的微信号,电话场景为空字符串
"intent": "是", // 话术配置了 intent 抽取字段时为该字段的值,否则与 intentionRank 相同(是/否)
"user_preference": "" // 话术配置了 user_preference 抽取字段时为该字段的值,否则为空字符串
},
"chatLogs": [ // 完整对话记录
{
"begin": "2024-01-01 12:00:00.120", // 本句开始时间(北京时间,精确到毫秒),可能为空字符串(见下方说明)
"end": "2024-01-01 12:00:05.480", // 本句结束时间(同上),可能为空字符串(见下方说明)
"role": "assistant", // 角色:assistant=AI,user=用户
"text": "您好,请问有什么可以帮您?"
}
],
"hangupType": "string", // 目前恒为 "normal",暂无实际区分意义,不建议依赖
"answerStatus": 0, // 目前恒为 0,不能用它判断是否接通,请用 callAnswerTime > 0 判断
"answerMainStatus": 0, // 目前恒为 0,同上
"wechatStatus": 0, // 目前恒为 0(该字段暂未接入落库),电话场景可忽略
"wechatSeatName": "string", // 目前恒为空字符串(微信人工坐席场景使用,电话场景无意义)
"wechatSeatPhone": "string", // 目前恒为空字符串,同上
"sipStatus": "string", // 目前恒为空字符串,暂无实际内容
"clueAttr": {}, // 线索字段透传:平台开启透传并选定字段后才有值,key 为线索字段的中文名称(如 {"订单号": "..."}),默认为空对象
"clueImportLabel": "string", // 线索导入批次标签,目前暂无法用于区分"平台网页测试拨打"与"正式 API 导入"
"extractData": { // 可选:平台开启「附带抽取字段」后才有,key 为数字员工意向抽取中配置的字段,值为字符串,未抽取到的为空字符串
"order_level": "A" // 示例:业务分级字段(字段名和取值由话术配置决定,见下方「意向字段取值」)
},
"meetingIds": ["string"], // 可选:本通电话办理过的会议业务单号(去重),见下方「MCP 工具办理记录」
"meetings": [ // 可选:每场会议具体办理了什么,见下方「MCP 工具办理记录」
{ "meetingId": "string", "action": "register", "names": ["string"] }
]
}
字段可用性说明:
hangupType、answerStatus、answerMainStatus、wechatStatus、wechatSeatName、wechatSeatPhone、sipStatus这 7 个字段目前在电话场景下都是固定值/占位值,暂时没有实际业务含义,不建议在这几个字段上写判断逻辑,后续如果补上真实取值会在此更新。clueImportLabel是真实字段,但目前无法用来区分测试拨打和正式线索;如果收到的话单phone不是有效手机号格式(比如一串随机字符)、caller为空,通常是平台内网页测试拨打产生的数据,可以直接忽略。
contextId关联线索:通过实时单条推送线索时传入contextId(如业务方订单号),话单回调将原样返回,可据此将话单对应至业务方的业务记录。未传contextId的线索(包括 Excel 批量上传、后台导入),该字段为空字符串,可使用phone+callHangupTime匹配,或调用「查询线索通话结果」/「批量查询线索」接口按phone/programId反查。
caller说明:这是线路配置的主叫账号(如250105ltwu),不是用户手机上看到的外显号码,也不一定是标准电话号码格式。如果需要外显号码和caller保持一致,可以在申请/配置线路时提出这个要求。
chatLogs[].begin/end为空:已知在部分对话被打断或应答极短的情况下,这两个时间戳可能返回空字符串,使用前请判空处理。具体触发条件仍在核实中,如对业务逻辑造成影响,请携带具体样本反馈。回调里没有
threadId/threadDetailId:WT 格式的话单不包含平台的线索 ID,请用上面的contextId关联;如果需要线索 ID,可以改用 VS 格式(带threadId)或下方的项目结算事件(带threadDetailId)。
extractData:WT 格式的intentionTag只有上面约定的 4 个字段。需要数字员工配置的全部抽取字段时,在话单推送中开启「附带抽取字段」,话单会多出extractData对象;未开启时没有这个字段。
意向字段取值:
| 字段 | 取值 | 说明 |
|---|---|---|
intentionRank | 是 / 否 | 是否有意向,只有这两个值 |
intentionStatus | 2 / 1 / 0 | 2=有意向(此时 intentionRank=是);1=无意向(intentionRank=否,含客户全程未回应);0=无结果(意向判断为模糊/待定,或本通电话 AI 分析未成功,此时 intentionRank=否) |
extractData.<分级字段> | 由话术配置决定 | 业务分级。例如宽带订单项目的 order_level:A=明确要装,B=犹豫,C=不想装,D=无效单 |
需要按业务分级处理时,请以
extractData里的分级字段为准,不要用intentionRank/intentionStatus推算分级——这两个字段只表示"是否有意向",和 A/B/C/D 不是一一对应的。
MCP 工具办理记录
数字员工在通话中调用 MCP 工具办理业务(如登记参会人、转派)时,可以把办理结果带进话单:meetingIds / meetings 说明这通电话实际办理了哪些业务单(如哪场会议)、做了什么操作。通话中没有办理记录时,话单不带这两个字段,其它字段不受影响。
meetingIds(字符串数组):本通电话办理过的业务单号,已去重。只需要知道是哪一单时,看这个字段即可。
meetings(对象数组):每一项表示「一个业务单 + 一种操作」。
| 字段 | 类型 | 说明 |
|---|---|---|
| meetingId | string | 业务单号,取自工具调用时传给接入方接口的参数(默认参数名 meetingId) |
| action | string | 本通电话对该业务单做的操作,取值见下方 |
| names | string[] | 该操作涉及的人员姓名(默认取参数 name,没有时取 toName),没有时为空数组 |
记录规则可按工具配置:在平台 MCP 工具配置中——
| 配置项 | 说明 | 默认 |
|---|---|---|
| 话单会议动作 | 填写后,该工具调用成功即记入话单,填写的值就是 action | 不填则不记录(查询类工具不填) |
| 话单记录单号参数 | 取工具的哪个参数作为 meetingId | meetingId |
| 话单记录人名参数 | 取哪些参数作为 names 里的人名,多个用逗号分隔,取第一个有值的 | name,toName |
以会议通知数字员工为例,当前配置的取值:
| action | 对应工具 | names |
|---|---|---|
register | 登记参会人 | 登记的参会人姓名,登记了几个人就有几个 |
delegate | 转派 | 被转派人姓名 |
后续新增工具(如撤销登记)时,配置对应的动作(如 cancel)即可出现在话单中,无需改动接口。
除工具配置的操作外,还有一个系统自动生成的取值:inquiry——呼入电话中没有任何办理操作,且按来电号码只查到一个业务单(如名下只有一场会议)时,带上这一单,names 为空数组。
规则:
- 只记录接口返回成功的调用,失败的不会出现
- 通话中有任何办理操作时,只报这些操作,不再带
inquiry - 同一业务单的同一种操作合并为一项;同一人重复办理(如中途修改提醒时间重新登记)只记一次
- 同一通电话对同一业务单做了多种操作时,每种操作各一项
"meetingIds": ["M20261022001"],
"meetings": [
{ "meetingId": "M20261022001", "action": "register", "names": ["王硕", "李娜"] },
{ "meetingId": "M20261022001", "action": "delegate", "names": ["张伟"] }
]
签名规则
1. 拼接签名字符串:
timestamp={timestamp}&nonce={nonce}&appId={appId}&data={body_json_string}&appSecret={appSecret}
2. 对拼接后的字符串做哈希,结果即为 X-Sign 的值:
X-Sign-Method: md5→ MD5X-Sign-Method: sha256→ SHA256
验签示例(Node.js):
const crypto = require('crypto')
function verifySign(headers, bodyStr, appSecret) {
const { 'x-timestamp': ts, 'x-nonce': nonce, 'x-app-id': appId,
'x-sign': sign, 'x-sign-method': method = 'md5' } = headers
const raw = `timestamp=${ts}&nonce=${nonce}&appId=${appId}&data=${bodyStr}&appSecret=${appSecret}`
const expected = crypto.createHash(method).update(raw).digest('hex')
return expected === sign
}
响应规范
| HTTP 状态码 | 说明 |
|---|---|
| 2xx | 接收成功,系统不再重推 |
| 非 2xx 或超时 | 接收失败,系统按策略重试(是否重试、重试次数/间隔需要在平台侧配置,见下方注意事项) |
注意事项
- 时间戳有效期: 建议拒绝距当前时间超过 5 分钟的请求
- Nonce 防重放: 建议记录已处理的 nonce,拒绝重复请求
- 幂等处理: 以
callId为唯一键,避免重复处理同一通话 - 快速响应: 接收方应尽快返回 2xx(超时时间在话单推送配置中设置,默认 30 秒),超时视为失败并按配置重推
- 失败重推: 推送失败(非 2xx)后是否重推、重推次数和间隔,是数字员工侧的可选配置,默认可能关闭;如业务方接口可能出现短暂不可用,建议联系平台开启并确认当前的重推参数。重推全部失败时,也可使用系统集成文档中的「查询线索通话结果」/「批量查询线索」接口兜底核对遗漏的话单。
- 未接通是否推送: 默认只推送接通的通话,未接通(空号、关机、无人接听、拒接等)不会收到回调,可通过「查询线索通话结果」/「批量查询线索」接口获取(看
reached字段)。开启「推送未接通话单」后,未接通的电话也推一条话单:callDurationSeconds=0、callAnswerTime=0、chatLogs为空、summary为空、intentionRank=否、intentionStatus=0;同一线索每次重拨各推一条。同时开着「只推接通话单」或「只推有意向话单」时仍不推未接通。开启「只推有意向话单」时,无意向的通话也不会收到回调。如发现部分通话未收到话单,请联系平台确认当前配置。 - 时序参考: 线索入队后通常约 3 秒内发起呼叫(用户手机端出现来电一般在 3-4 秒内);通话挂断后话单回调通常在约 40 秒内推送。具体耗时会随线路和系统负载波动,仅供参考。
VS 格式(通用 JSON)
选择 VS 格式时,话单以结构化 JSON 推送,认证方式、请求头、签名均可自定义。
请求 Header
| 参数名 | 出现条件 | 说明 |
|---|---|---|
| Content-Type | 总是 | application/json |
| Authorization | 认证方式为 Basic Auth / Bearer Token | Basic <base64(密钥)> 或 Bearer <Token>;Basic Auth 的密钥请填 用户名:密码 |
| X-API-Key | 认证方式为 API Key | 填写的密钥 |
| X-Signature | 配置了签名密钥和签名算法 | 签名值,见下方签名规则 |
| X-Signature-Method | 同上 | md5 / sha256 / hmac |
| 自定义请求头 | 配置了自定义请求头 | 按配置原样发送 |
请求 Body
{
"dailyNumber": 12, // 当天该项目第几条 API 推送(从 1 开始)
"threadId": "string", // 平台线索 ID
"callId": "string", // 话单唯一标识(以此字段做幂等)
"contextId": "string", // 推送线索时传入的 contextId,原样返回
"direction": "outbound", // outbound=外呼 inbound=呼入
"phoneNumber": "13800138000", // 客户手机号
"seatName": "string", // 仅人工外呼的话单有:拨打坐席姓名
"callTime": { // 通话各时间点(北京时间 YYYY-MM-DD HH:mm:ss)
"make": "2026-10-22 10:00:00", // 发起
"ring": "2026-10-22 10:00:05", // 振铃
"answer": "2026-10-22 10:00:10", // 接通(未接通时为 1970-01-01 08:00:00)
"hangup": "2026-10-22 10:03:20" // 挂机
},
"intention": {
"rank": "是", // 是否有意向:固定为 "是" / "否"(A-D 分级看下面的 level)
"status": 1, // 意向状态:1=有意向 2=无意向 0=未判断
"text": "高意向", // 意向文字
"rate": "80", // 意向评分
"title": "string", // 以下 4 个仅在配置了核心判断时出现:判断名称
"level": "A", // 判断等级 A-D
"levelLabel": "string", // 等级文字
"reason": "string" // 判断说明
},
"summary": "string", // AI 通话摘要
"content": { "items": [] }, // 完整对话记录(平台原始结构)
"recordUrl": "string", // 录音下载地址
"extractData": { "key1": "value1" }, // 数字员工意向抽取的全部字段
"clueData": { "字段名": "值" }, // 仅开启线索字段透传时有
"timestamp": 1792540800000, // 推送时间(毫秒时间戳)
"meetingIds": ["string"], // 可选,见上方「MCP 工具办理记录」
"meetings": [] // 可选,见上方「MCP 工具办理记录」
}
签名规则
配置了签名密钥(secret)时,X-Signature 按 X-Signature-Method 计算,body 为收到的原始请求体字符串:
| X-Signature-Method | 计算方式 |
|---|---|
| md5 | md5(body + secret) 的十六进制 |
| sha256 | sha256(body + secret) 的十六进制 |
| hmac | HMAC-SHA256(secret, body) 的十六进制 |
const crypto = require('crypto')
function verifyVs(rawBody, headers, secret) {
const method = headers['x-signature-method']
const expected = method === 'hmac'
? crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
: crypto.createHash(method).update(rawBody + secret).digest('hex')
return expected === headers['x-signature']
}
响应规范、重推与注意事项同 WT 格式。请求方法选 GET 时不带 body,只能收到请求头,一般请使用 POST。
平台配置页的「请求示例」按钮会按当前配置生成一份完整示例(含请求头与 body),可直接发给接入方参考。
项目结算事件
每通电话结算后,向项目配置的地址 POST 一条事件。按项目配置,未配置则不推送。
配置接口
GET /api/v1/external/programs/:programId/webhook — 查询当前配置(所需权限:READ)
{ "code": 0, "data": { "programId": "cmk...", "webhookUrl": "https://example.com/hook", "hasSecret": true } }
PUT /api/v1/external/programs/:programId/webhook — 设置或修改(所需权限:CONTROL)
| 字段 | 类型 | 说明 |
|---|---|---|
| webhookUrl | string | null | 推送地址,需以 http:// 或 https:// 开头;传 null 关闭推送 |
| webhookSecret | string | null | 签名密钥,设置后每次推送都带签名;传 null 取消签名 |
两个字段都可选,只传需要改的。创建项目时(1.6 创建项目)也可以直接带上这两个字段。修改后最多 5 分钟内生效。
curl -X PUT "https://api.halfcall.cn/api/v1/external/programs/cmk.../webhook" \
-H "Authorization: Bearer sk_xxx" \
-H "Content-Type: application/json" \
-d '{ "webhookUrl": "https://example.com/hook", "webhookSecret": "your_secret" }'
请求 Header
| 参数名 | 说明 |
|---|---|
| Content-Type | application/json |
| X-SmartCall-Event | 事件类型,目前固定为 call.settled |
| X-SmartCall-Delivery | 本次推送的唯一 ID(即该通通话的 taskDetailId),可用于幂等 |
| X-SmartCall-Signature | 配置了 webhookSecret 时才有:对原始请求 body 做 HMAC-SHA256(密钥为 webhookSecret)后的十六进制字符串 |
请求 Body
{
"event": "call.settled",
"timestamp": "2026-10-14T02:30:00.000Z",
"data": {
"programId": "cmk...",
"taskDetailId": "string", // 本通通话 ID
"threadDetailId": "string", // 线索 ID,与推送线索接口返回的 threadId 一致
"phone": "13800138000",
"stage": 1, // 所在环节
"isReached": true, // 是否接通(请用这个字段判断)
"duration": 63, // 通话时长(秒)
"isIntention": true, // 是否有意向
"intention": "HIGH", // 意向:NONE=无意向 REACHED=仅接听 HIGH=意向 HESITATE=模糊意向
// LOW_DIFFICULTY/HIGH_DIFFICULTY=沟通难度低/高 UNKNOW=未知
"intentionRate": 0, // 意向评分
"reached": "string", // 内部状态值,可能是中间态,不建议依赖,请用 isReached
"endTime": "2026-10-14T02:30:00.000Z"
}
}
和话单回调不同,这里带了
threadDetailId,可以直接对应到推送线索时拿到的线索 ID。事件在通话结算时发出,此时 AI 的意向分析可能还没跑完,intention以查询线索通话结果接口为准。
验签示例(Node.js)
const crypto = require('crypto')
function verify(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return expected === signature
}
注意事项
- 请在 10 秒内返回 2xx,超时视为失败
- 失败不重推。如果对完整性有要求,请定期用批量查询线索接口对账
- 验签必须使用收到的原始 body 字符串,不要先解析再序列化