Webhook 事件
回调要求
- 必须使用公网 HTTPS 地址;平台不跟随重定向。
- 收到请求后尽快完成签名校验并返回 HTTP 2xx。
- 复杂业务放入异步队列,避免因为处理超时触发重复投递。
- 使用
eventId做全局幂等;消息类事件同时使用业务messageId做幂等。
请求头
X-Open-App-Id
X-Open-Event-Id
X-Open-Event-Type
X-Open-Timestamp
X-Open-Signature
管理订阅(控制台 API)
订阅的读写走控制台 API,使用开发者登录态而不是应用签名:
- 基地址
https://imo.lanchr.com/api/open/console/v1(测试环境https://im-open-test.lanchr.com/api/open/console/v1); - 请求头带
Authorization: Bearer <accessToken>与X-Open-Tenant-Id: <租户ID>,与其它控制台接口一致(见鉴权与签名 → 控制台 API); - 一次
PUT需要一个事件类型;应用有多个事件类型时逐个配置。
| 接口 | 方法 | 说明 |
|---|---|---|
/apps/{appId}/subscriptions | GET | 列出全部订阅 |
/apps/{appId}/subscriptions/{eventType} | PUT | 新建或更新某个事件类型的订阅,每次都重新生成 Secret |
/apps/{appId}/subscriptions/{eventType}/verify | POST | 触发一次回调地址验证 |
GET 返回订阅数组,每项包含 eventType、callbackUrl、status、failureCount、circuitState、updatedAt。
PUT 请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
callbackUrl | string | 是 | 回调地址,必须为公网 HTTPS 地址 |
status | string | 否 | 订阅状态,默认 ACTIVE;只有 ACTIVE 的订阅会收到投递 |
callbackUrl 会被校验:必须使用 HTTPS、主机名可解析、且解析结果全部是公网地址(拒绝回环、内网、链路本地等地址)。
获取 Webhook Secret
Webhook Secret 不是应用凭证(appSecret),而是每个事件订阅各自独立的密钥,用于校验该订阅的投递签名。
PUT /apps/{appId}/subscriptions/{eventType} 在保存订阅的同时生成 Secret 并一次性返回:
{"credentialId": "<eventType>", "secret": "<Webhook-Secret>"}
credentialId 就是事件类型本身,secret 为 32 字节随机数的 Base64URL 编码(无 = 填充)。
⚠️ 每次
PUT都会轮换 Secret,旧 Secret 立即失效——即使请求体内容完全相同。GET永不回显 Secret。请务必在PUT的响应中把 Secret 落库;错过之后只能再PUT一次换取新 Secret,并同步更新你的验签配置。
签名
Webhook 签名原文为:
eventId + "\n" + timestampMillis + "\n" + rawBody
使用 Webhook Secret(非应用 appSecret)计算 HMAC-SHA256,输出小写十六进制。必须基于原始请求体计算,不能先解析 JSON 再序列化。
伪代码:
const value = eventId + '\n' + timestamp + '\n' + rawBody
const signature = hmacSha256(webhookSecret, value).toLowerCaseHex()
// 与请求头 X-Open-Signature 做等时比较
校验时还应检查 X-Open-Timestamp 的时效性:建议与平台保持一致,接受前后 ±5 分钟的时间戳窗口,并对同一 eventId 去重以防重放。
验证回调地址
用 POST /api/open/console/v1/apps/{appId}/subscriptions/{eventType}/verify 触发地址验证(无请求体)。平台会向该订阅的 callbackUrl 发送:
{"type": "url_verification", "challenge": "<uuid>"}
请求头为 Content-Type: application/json; charset=UTF-8,不携带任何签名请求头(没有 X-Open-*),因此无需验签。
你的服务只需识别 type == "url_verification" 并返回任意 2xx 即可通过验证。平台只看响应状态码,不比对回显内容,所以回显 challenge 只是良好的实现习惯:
{"challenge": "<收到的 challenge>"}
接口返回 {"code":"000000","value":true} 表示验证通过,value:false 表示平台没有收到 2xx。该订阅不存在或不是 ACTIVE 时调用失败。
响应和重试
返回任意 2xx 表示接收成功。业务处理失败时不要返回 2xx 后静默丢弃,应保存事件并由业务队列重试。
投递使用以下超时:连接 3 秒、读取 8 秒,不跟随重定向。以下情况会重试:
- 连接失败、超时等网络错误;
- HTTP
408、409、425、429,或任意5xx。
其它非 2xx(如 400、403、404)不会重试,事件会被直接判为投递失败并终止。最多重试 6 次,退避间隔依次为:
| 第 n 次重试 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 间隔 | 10 秒 | 30 秒 | 2 分钟 | 10 分钟 | 30 分钟 | 2 小时 |
用尽 6 次重试后事件进入终止状态,不再投递。
熔断:熔断按应用维度统计。10 秒滑动窗口内投递达到 5 次且失败率不低于 50% 时,该应用的 Webhook 投递被熔断 30 秒;这期间新事件不会被投递,而是延后 30 秒再排队。30 秒后放行 1 次探测请求,成功则恢复正常,失败则继续熔断。
投递记录与失败原因可在控制台「投递」页查看。
事件只在投递时存在
ACTIVE订阅才会发出;此时若该事件类型没有有效订阅,事件会被直接丢弃,不会因为之后补建订阅而追发。请在应用上线前配置好订阅。
事件字段清单见事件模型。