Skip to content

回调说明

登录成功后,企微里收到的消息和发生的事件,会由系统以 POST 请求推送到你设置的回调地址。

三步接上

1. 设置回调地址。 调用设置或更新回调地址传入 urlevents 不填表示订阅全部事件。

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=truedata.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 或域名),接收 POST JSON。开发阶段没有公网地址,见本地联调
  • 验签、解析并可靠入队成功后,及时返回空 JSON 对象 {}(HTTP 2xx),整个接收过程要在 5 秒内完成;耗时业务处理由队列消费者异步做。入队失败时不要返回 2xx,否则系统会认为已接收成功。
  • 同一条推送可能重复送达,要分两层去重:推送层用请求头 X-Eyun-Delivery,消息层用消息 id(同一账号内唯一)。
  • idsyncKeyfromUserIdtoUserIdroomId 是 int64 大整数,解析时不要转成 JS Number 或浮点数。

可靠接收的必做项:

  • 每次推送都记录 X-Eyun-EventX-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好友申请或新增,contentType2357 / 2132一条系统通知,见回调字典 · 系统通知
friend.deleted删除外部联系人,伴随 2131;需同步外部数据判断增删同上
contact.modified2131 外部联系人变动,以及 2118 / 1006 群或联系人变动同上
instance.online实例的企微连接建立{}
instance.offline实例的企微连接断开{code, message, time},见回调字典 · 连接事件
webhook.test调用测试投递测试用,返回 HTTP 2xx(建议返回 {}

能订阅哪些事件,以可订阅事件列表接口返回为准。

参考写法(Node.js + Express,queueonText 等换成你自己的实现):

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=falsedata.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=truedata.httpStatus 为 2xx配置完成
收不到推送测试投递响应里的 data.okdata.httpStatus;确认地址公网可访问、接收 POST;用查询当前回调配置核对 urlstatus 应为 active
签名校验不通过确认用的是首次创建时返回的 secret,并用原始请求体计算;丢失后先删除配置重新设置获取新密钥,同时更新验签密钥,或联系对接人
同一条收到多次X-Eyun-Delivery 去重;消息层再按 id 去重(同一账号内唯一)
处理耗时长验签并可靠入队后返回 {},耗时业务处理在消费者中异步执行
发了消息,没收到正文推送正常:经接口发出的消息只推已读回执 2001
账号掉线,没收到任何事件-11001 / -11002 都会推送 instance.offline;检查事件订阅和回调状态,并用业务接口探测,按掉线检测与恢复处理

下一步