Appearance
回调说明
登录成功后,企微里收到的消息和发生的事件,会由系统以 POST 请求推送到你设置的回调地址。
三步接上
1. 设置回调地址。 调用设置或更新回调地址传入 url;events 不填表示订阅全部事件。
http
POST {BASE_URL}/wx-api/webhook/set
Authorization: Bearer <你的 App Token>
Content-Type: application/json
{
"url": "https://your.domain/hook"
}响应里的 data.secret 只在首次创建时返回一次,保存到服务端,用来校验签名。每个应用只有一个回调地址,再次调用是更新,不返回也不改 secret。
如果密钥丢了,先删除回调配置,再重新设置获取新 secret,同时更新服务端验签密钥;也可联系对接人。删除会立即停止推送,操作前请确认。
2. 测试一下。 调用测试投递,系统会向你的地址推送一条 webhook.test。确认响应里的 data.ok=true 且 data.httpStatus 为 2xx,才算测试通过;data.httpStatus 是你的地址返回的状态,不能只看测试接口本身的 HTTP 200。
http
POST {BASE_URL}/wx-api/webhook/test
Authorization: Bearer <你的 App Token>
Content-Type: application/json
{}3. 接收推送。 之后每条消息或事件都是一次 POST,按下面的方法处理。
回调地址要求
- 公网可访问(公网 IP 或域名),接收
POSTJSON。开发阶段没有公网地址,见本地联调。 - 验签、解析并可靠入队成功后,及时返回空 JSON 对象
{}(HTTP 2xx),整个接收过程要在 5 秒内完成;耗时业务处理由队列消费者异步做。入队失败时不要返回 2xx,否则系统会认为已接收成功。 - 同一条推送可能重复送达,要分两层去重:推送层用请求头
X-Eyun-Delivery,消息层用消息id(同一账号内唯一)。 id、syncKey、fromUserId、toUserId、roomId是 int64 大整数,解析时不要转成 JS Number 或浮点数。
可靠接收的必做项:
- 每次推送都记录
X-Eyun-Event、X-Eyun-Delivery、处理耗时与处理结果。 - 不认识的事件也返回
{},不要返回 5xx。 - 日志要脱敏,消息正文、姓名、手机号等不要原样写进日志。
接口发送不推正文,掉线按错误码处理
经接口发出的消息不推正文,只推一条已读回执(contentType 2001)。
-11001 和 -11002 都会推送 instance.offline。只有 -11001 可以自动重连;-11002 应置为离线,提示「已在其他设备登录」,由用户决定是否重新登录。仍需保留定期业务探测,见实例与代理 · 掉线检测与恢复。
收到推送后怎么处理
每次推送带三个请求头:
| 请求头 | 含义 |
|---|---|
X-Eyun-Event | 事件名,见下表 |
X-Eyun-Delivery | 这次推送的唯一 id,用来去重 |
X-Eyun-Signature | 签名,校验方法见校验签名 |
| 事件名 | 什么时候推 | 请求体 |
|---|---|---|
message.received | 收到文本、图片、视频等内容类消息 | 一条消息,见回调字典 |
friend.added | 好友申请或新增,contentType 为 2357 / 2132 | 一条系统通知,见回调字典 · 系统通知 |
friend.deleted | 删除外部联系人,伴随 2131;需同步外部数据判断增删 | 同上 |
contact.modified | 2131 外部联系人变动,以及 2118 / 1006 群或联系人变动 | 同上 |
instance.online | 实例的企微连接建立 | {} |
instance.offline | 实例的企微连接断开 | {code, message, time},见回调字典 · 连接事件 |
webhook.test | 调用测试投递时 | 测试用,返回 HTTP 2xx(建议返回 {}) |
能订阅哪些事件,以可订阅事件列表接口返回为准。
参考写法(Node.js + Express,queue、onText 等换成你自己的实现):
queue.push 是你接入的持久化队列适配器,必须在消息可靠保存后才完成,失败时抛出异常;不能用内存数组的 push 代替。消费者再执行 handle,并实现去重、失败重试与处理结果记录。
js
import express from 'express'
import JSONbig from 'json-bigint' // 大整数不丢精度
import { verify } from './verify.js' // 见下文「校验签名」
const app = express()
app.post('/hook', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.EYUN_WEBHOOK_SECRET
const signature = req.get('X-Eyun-Signature') ?? ''
const ok = verify(secret, req.body, signature)
if (!ok) return res.status(401).end()
try {
await queue.push({
delivery: req.get('X-Eyun-Delivery'), // 用它去重
event: req.get('X-Eyun-Event'),
body: JSONbig.parse(req.body.toString('utf8')),
})
} catch {
return res.status(500).end() // 未可靠接收,让系统按规则重试
}
res.json({}) // 已可靠入队,耗时业务逻辑由消费者处理
})
// 联系人类事件,按 contentType 查回调字典
const CONTACT_EVENTS = [
'friend.added',
'friend.deleted',
'contact.modified',
]
// 队列里逐条处理
function handle({ event, body }) {
if (event === 'message.received') {
switch (Number(body.contentType)) {
case 0: case 2:
return onText(body.content.map((c) => c.text).join(''))
case 14: case 101: case 16: case 23:
case 103: case 15: case 20:
// 用 content.id 与 aesKey 调 CDN 下载
return onMedia(body)
case 2001:
return // 已读回执,可忽略
default:
// 其它类型原样保存,读法查回调字典
return saveRaw(body)
}
}
if (CONTACT_EVENTS.includes(event)) return onContact(body)
if (event === 'instance.offline') return alert(body)
if (event === 'webhook.test') return
if (event === 'instance.online') return
return saveRaw(body) // 其它事件原样保存,由业务判断
}推送规则
| 规则 | 说明 |
|---|---|
| 超时 | 每次推送超时为 5 秒 |
| 成功 | 回调地址返回 HTTP 2xx 即成功,建议返回 {} |
| 失败重试 | 最多尝试 5 次;退避间隔依次为 1 分钟、5 分钟、30 分钟、2 小时、6 小时 |
| 顺序 | 不保证顺序;消息可按 sendTime 排序 |
| 去重 | 推送层用 X-Eyun-Delivery,消息层用消息 id(同一账号内唯一) |
| 连续失败 | 会自动熔断并暂停推送;修好后去控制台的 Webhooks 页手动恢复 |
控制台地址为 https://qiwei.eyunz.com。
暂停或熔断期间的事件不会补发
回调状态为 paused(暂停)或 circuit_open(熔断)期间,事件直接丢弃,不积压。恢复后只接收新事件,期间的事件也不补发。
测试投递失败时,接口本身仍可能返回 HTTP 200,结果中 data.ok=false;data.httpStatus 表示回调地址返回的状态。判断测试是否成功要看推送结果,不能只看接口 HTTP 状态或 code=0。
校验签名
每次推送带请求头 X-Eyun-Signature: sha256=<hmac_sha256(secret, body)>,body 是收到的原始请求体字节,校验时用常量时间比较。
js
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(secret, rawBody, header) {
const hmac = createHmac('sha256', secret)
const digest = hmac.update(rawBody).digest('hex')
const expected = 'sha256=' + digest
const a = Buffer.from(expected), b = Buffer.from(header)
return a.length === b.length && timingSafeEqual(a, b)
}python
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
expected = 'sha256=' + digest
return hmac.compare_digest(expected, header)验证与排查
| 现象 | 怎么办 |
|---|---|
测试推送的 data.ok=true 且 data.httpStatus 为 2xx | 配置完成 |
| 收不到推送 | 看测试投递响应里的 data.ok 与 data.httpStatus;确认地址公网可访问、接收 POST;用查询当前回调配置核对 url,status 应为 active |
| 签名校验不通过 | 确认用的是首次创建时返回的 secret,并用原始请求体计算;丢失后先删除配置再重新设置获取新密钥,同时更新验签密钥,或联系对接人 |
| 同一条收到多次 | 按 X-Eyun-Delivery 去重;消息层再按 id 去重(同一账号内唯一) |
| 处理耗时长 | 验签并可靠入队后返回 {},耗时业务处理在消费者中异步执行 |
| 发了消息,没收到正文推送 | 正常:经接口发出的消息只推已读回执 2001 |
| 账号掉线,没收到任何事件 | -11001 / -11002 都会推送 instance.offline;检查事件订阅和回调状态,并用业务接口探测,按掉线检测与恢复处理 |