开发者中心-事件订阅 接入指南
概述
开发者中心为企业提供 Webhook 订阅能力,当 CRM 内发生业务事件时,系统会主动推送事件通知到企业配置的回调地址。
快速开始
- 开通功能:联系管理员在后台「设置 - 开发者中心」开通该功能
- 配置回调地址:填写接收事件的 HTTPS 回调地址
- 获取签名密钥:系统生成唯一的
subscribe_secret,用于验签 - 选择订阅事件:勾选需要接收的事件类型
- 验证推送:点击「测试推送」,确认能收到
test.ping事件并验签通过 - 处理事件:在回调接口实现验签、业务处理逻辑
每个事件的 event_type 格式为 {entity}.{action},例如 customer.update、opportunity.won。
推送格式
请求结构
所有事件推送均为 POST 请求,Content-Type 为 application/json:
POST {callback_url}
Content-Type: application/json
X-LXY-Signature: {HMAC-SHA256 签名}
X-LXY-Timestamp: {Unix 时间戳,秒}
X-LXY-Nonce: {随机字符串}
{JSON payload}
Payload 结构
{
"event_id": "evt_a1b2c3d4e5f6",
"event_type": "customer.update",
"occurred_at": "2026-09-01T14:30:00+08:00",
"organization_id": 123,
"operator": {
"id": 456,
"name": "张三"
},
"entity": {
"type": "Customer",
"id": 789
},
"data": {
"changes": {
"name": ["旧公司名", "新公司名"]
}
}
}
字段说明:
event_id:全局唯一事件标识,格式evt_{16位hex}event_type:事件类型,格式{entity}.{action}occurred_at:事件发生时间(ISO 8601 格式)organization_id:企业 IDoperator:操作人信息(后台任务触发时为null)entity:实体类型和 ID,不含完整数据(需通过 API 查询详情)data:事件专属业务数据(见下方示例)
常见 Payload 示例
客户创建
{
"event_type": "customer.create",
"entity": { "type": "Customer", "id": 300 },
"data": {}
}
客户更新(含自定义字段)
{
"event_type": "customer.update",
"entity": { "type": "Customer", "id": 300 },
"data": {
"changes": {
"name": ["旧公司名", "新公司名"],
"note": ["11", "22"],
"text_asset_dec3c0": ["a", "b"]
}
}
}
说明:
changes包含所有变更字段(本表字段 + 自定义字段)- 自定义字段推送原始值(ID、原始数据),不含显示值
- 用户/部门字段格式为 ID 数组:
[[旧ID数组], [新ID数组]] - 日期字段格式为 ISO 8601 字符串
客户转移
{
"event_type": "customer.transfer",
"entity": { "type": "Customer", "id": 300 },
"data": {
"from_user": { "id": 201, "name": "王五" },
"to_user": { "id": 202, "name": "赵六" }
}
}
客户标签更新
{
"event_type": "customer.label_update",
"entity": { "type": "Customer", "id": 300 },
"data": {
"changes": {
"labels": [
[{"id": 1, "content": "重点客户"}, {"id": 2, "content": "VIP"}], //旧值
[{"id": 2, "content": "VIP"}, {"id": 3, "content": "潜在客户"}] //新值
]
}
}
}
商机赢单
{
"event_type": "opportunity.won",
"entity": { "type": "Opportunity", "id": 400 },
"data": {
"from_stage": { "id": "12", "name": "商务谈判" },
"to_stage": { "id": "18", "name": "赢单" }
}
}
签到创建
{
"event_type": "checkin.create",
"entity": { "type": "Checkin", "id": 500 },
"data": {}
}
签名验证
签名算法
系统使用 HMAC-SHA256 对推送内容签名:
signature = HMAC-SHA256(subscribe_secret, "{timestamp}.{nonce}.{body}")subscribe_secret:签名密钥timestamp:请求头X-LXY-Timestamp的值(Unix 时间戳秒)nonce:请求头X-LXY-Nonce的值(随机字符串)body:原始请求 body(JSON 字符串)
验证步骤
- 从请求头取
X-LXY-Signature、X-LXY-Timestamp、X-LXY-Nonce - 读取原始 body 字符串(不要先解析 JSON 再序列化)
- 用
subscribe_secret对"{timestamp}.{nonce}.{body}"计算 HMAC-SHA256 - 比对计算结果与请求头的签名值)
- 可选:校验
timestamp在 5 分钟内,防止重放攻击
示例代码
Python
import hmac
import hashlib
import time
def verify_webhook_signature(subscribe_secret, timestamp, nonce, body, signature):
expected = hmac.new(
subscribe_secret.encode('utf-8'),
f"{timestamp}.{nonce}.{body}".encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook_callback():
body = request.get_data(as_text=True)
signature = request.headers.get('X-LXY-Signature')
timestamp = request.headers.get('X-LXY-Timestamp')
nonce = request.headers.get('X-LXY-Nonce')
if not verify_webhook_signature(os.environ['SUBSCRIBE_SECRET'], timestamp, nonce, body, signature):
return jsonify({'error': 'Invalid signature'}), 401
# 可选:防重放
if abs(time.time() - int(timestamp)) > 300:
return jsonify({'error': 'Request too old'}), 400
payload = request.get_json()
# 处理事件...
return jsonify({'success': True}), 200
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(subscribeSecret, timestamp, nonce, body, signature) {
const expected = crypto
.createHmac('sha256', subscribeSecret)
.update(`${timestamp}.${nonce}.${body}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express
const express = require('express');
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body.toString('utf8');
const signature = req.headers['x-lxy-signature'];
const timestamp = req.headers['x-lxy-timestamp'];
const nonce = req.headers['x-lxy-nonce'];
if (!verifyWebhookSignature(process.env.SUBSCRIBE_SECRET, timestamp, nonce, body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 可选:防重放
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(400).json({ error: 'Request too old' });
}
const payload = JSON.parse(body);
// 处理事件...
res.json({ success: true });
});
响应要求
成功响应
返回 HTTP 状态码 2xx(200/201/204 均可),响应 body 不限:
HTTP/1.1 200 OK
Content-Type: application/json
{"success": true}
失败处理
- 返回 非 2xx 状态码 或 连接超时(2 秒)视为失败
- 推送不会自动重试,失败后事件日志状态标记为
failed - 若需重新推送,需自行实现补偿机制
最佳实践
- 快速响应:尽快返回 200,将耗时业务逻辑放入队列异步处理
- 记录原始 payload:即使解析失败也能事后排查
- 幂等处理:同一
event_id可能推送多次(网络重试),需保证幂等 - 监控告警:统计失败率和处理延迟
- 定期对账:关键业务场景建议定时全量对账
测试推送
后台「开发者中心」提供「测试推送」按钮,发送 test.ping 事件:
{
"event_id": "evt_test1234567890ab",
"event_type": "test.ping",
"occurred_at": "2026-09-01T10:00:00+08:00",
"organization_id": 123,
"operator": null,
"entity": { "type": "Test", "id": 0 },
"data": { "message": "This is a test event" }
}
常见问题
1. 如何获取实体完整数据?
Webhook 推送只包含实体的 type 和 id,不包含完整数据。需要通过 CRM API 根据 ID 查询详情。
2. 自定义字段的值是什么格式?
- 选择字段:选项 ID(字符串)
- 多选字段:逗号分隔的选项 ID 字符串,如
"mul_xx1,mul_xx2,mul_xx3" - 用户/部门字段:ID 数组,如
[1, 2, 3] - 日期字段:ISO 8601 格式字符串,如
"2026-09-01" - 日期时间字段:ISO 8601 格式字符串,如
"2026-09-01T14:30:00+08:00" - 文本/数值字段:原始值
3. 为什么收不到某些事件?
检查以下几点:
- 确认企业已开通开发者中心功能
- 确认已勾选该事件类型
- 确认未超出配额限制
- 检查回调地址是否可访问(HTTPS,2秒内响应)
文档更新时间: 2026-09-04 16:09 作者:曹勇