事半AI

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-Typestring✓application/json
X-App-Idstring✓应用 ID
X-Timestampint64✓当前时间戳(秒)
X-Noncestring✓随机字符串(防重放)
X-Signstring✓签名值
X-Sign-Methodstring✓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是 / 否是否有意向,只有这两个值
intentionStatus2 / 1 / 02=有意向(此时 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(对象数组):每一项表示「一个业务单 + 一种操作」。

字段类型说明
meetingIdstring业务单号,取自工具调用时传给接入方接口的参数(默认参数名 meetingId)
actionstring本通电话对该业务单做的操作,取值见下方
namesstring[]该操作涉及的人员姓名(默认取参数 name,没有时取 toName),没有时为空数组

记录规则可按工具配置:在平台 MCP 工具配置中——

配置项说明默认
话单会议动作填写后,该工具调用成功即记入话单,填写的值就是 action不填则不记录(查询类工具不填)
话单记录单号参数取工具的哪个参数作为 meetingIdmeetingId
话单记录人名参数取哪些参数作为 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 → MD5
  • X-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 或超时接收失败,系统按策略重试(是否重试、重试次数/间隔需要在平台侧配置,见下方注意事项)

注意事项

  1. 时间戳有效期: 建议拒绝距当前时间超过 5 分钟的请求
  2. Nonce 防重放: 建议记录已处理的 nonce,拒绝重复请求
  3. 幂等处理: 以 callId 为唯一键,避免重复处理同一通话
  4. 快速响应: 接收方应尽快返回 2xx(超时时间在话单推送配置中设置,默认 30 秒),超时视为失败并按配置重推
  5. 失败重推: 推送失败(非 2xx)后是否重推、重推次数和间隔,是数字员工侧的可选配置,默认可能关闭;如业务方接口可能出现短暂不可用,建议联系平台开启并确认当前的重推参数。重推全部失败时,也可使用系统集成文档中的「查询线索通话结果」/「批量查询线索」接口兜底核对遗漏的话单。
  6. 未接通是否推送: 默认只推送接通的通话,未接通(空号、关机、无人接听、拒接等)不会收到回调,可通过「查询线索通话结果」/「批量查询线索」接口获取(看 reached 字段)。开启「推送未接通话单」后,未接通的电话也推一条话单:callDurationSeconds=0、callAnswerTime=0、chatLogs 为空、summary 为空、intentionRank=否、intentionStatus=0;同一线索每次重拨各推一条。同时开着「只推接通话单」或「只推有意向话单」时仍不推未接通。开启「只推有意向话单」时,无意向的通话也不会收到回调。如发现部分通话未收到话单,请联系平台确认当前配置。
  7. 时序参考: 线索入队后通常约 3 秒内发起呼叫(用户手机端出现来电一般在 3-4 秒内);通话挂断后话单回调通常在约 40 秒内推送。具体耗时会随线路和系统负载波动,仅供参考。

VS 格式(通用 JSON)

选择 VS 格式时,话单以结构化 JSON 推送,认证方式、请求头、签名均可自定义。

请求 Header

参数名出现条件说明
Content-Type总是application/json
Authorization认证方式为 Basic Auth / Bearer TokenBasic <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计算方式
md5md5(body + secret) 的十六进制
sha256sha256(body + secret) 的十六进制
hmacHMAC-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)

字段类型说明
webhookUrlstring | null推送地址,需以 http:// 或 https:// 开头;传 null 关闭推送
webhookSecretstring | 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-Typeapplication/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 字符串,不要先解析再序列化