Appearance
事件订阅
组织发生成员变动、你的应用被安装/卸载/调整授权时,机灵兔会主动 POST 通知你的 后端(webhook)——不用轮询通讯录,也不用让管理员手动同步。
1. 配置回调地址
在客户端「组织」→「应用」→ 应用设置 →「事件订阅」里填回调地址(https,本地 调试可用 http://localhost:*)并勾选关心的事件,点「保存并验证回调地址」。
保存前平台会向该地址发一次验证请求:
json
// POST 你的回调地址
{ "type": "url_verification", "challenge": "a1b2c3…" }你的端点必须返回 HTTP 200 + 原样回显 challenge:
json
{ "challenge": "a1b2c3…" }回显一致才保存成功。留空回调地址即关闭订阅。
2. 事件推送格式
json
// POST 你的回调地址
{
"event_id": "e8f0…",
"type": "org.member.joined",
"occurred_at": "2026-08-25T10:00:00+00:00",
"data": { "org_id": "org_1", "user_id": "u_123", "nickname": "张三" }
}请求头:
| Header | 说明 |
|---|---|
X-Jlt-Event | 事件类型(见下表) |
X-Jlt-Event-Id | 事件 id(幂等去重用,同一事件重试时不变) |
X-Jlt-Timestamp | 发送时间(unix 秒) |
X-Jlt-Signature | hex(HmacSHA256(app_secret, "<timestamp>." + 请求体原文)) |
验签(务必做):用 app_secret 对 <X-Jlt-Timestamp> 的字符串值 + "." + 请求体 原始字节(不要 re-serialize 过的 JSON)复算 HmacSHA256,与 X-Jlt-Signature 常量时间比对;并拒绝时间戳过老(如超过 1 小时)的请求防重放。
返回 2xx 表示接收成功;非 2xx 或超时(10s)会自动重试,退避间隔 30s → 2m → 10m → 1h,共 5 次尝试后标记失败(终态)。同一事件重试时 X-Jlt-Event-Id 不变——建议按它做幂等。
3. 可订阅事件
| 事件 | data 字段 | 说明 |
|---|---|---|
org.member.joined | org_id, user_id, nickname | 成员加入你的组织 |
org.member.left | org_id, user_id | 成员离开(被移除或主动退出) |
app.installed | org_id, granted_scopes | 你的应用被某组织安装(市场应用) |
app.uninstalled | org_id | 你的应用被某组织卸载 |
app.scopes.granted | org_id, granted_scopes | 安装方组织调整了授予的能力 |
4. 接收端示例(Node.js / Express)
js
import crypto from "node:crypto";
import express from "express";
const APP_SECRET = process.env.JLT_APP_SECRET;
const app = express();
// 注意:验签要的是请求体原始字节,先拿 raw body 再 parse。
app.use("/jlt/events", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Jlt-Timestamp") || "";
const sig = req.header("X-Jlt-Signature") || "";
const expected = crypto
.createHmac("sha256", APP_SECRET)
.update(ts + "." + req.body.toString("utf8"))
.digest("hex");
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).json({ error: "bad signature" });
}
if (Math.abs(Date.now() / 1000 - Number(ts)) > 3600) {
return res.status(401).json({ error: "stale" });
}
const event = JSON.parse(req.body.toString("utf8"));
// ① 配置回调时的验证请求:原样回显 challenge
if (event.type === "url_verification") {
return res.json({ challenge: event.challenge });
}
// ② 按 event_id 幂等处理事件
console.log("收到事件", event.type, event.data);
// ③ 尽快 2xx;耗时逻辑丢队列异步做,超时(10s)会被判定失败重试
res.json({ ok: true });
});
app.listen(3000);5. 常见问题
- 保存时报「回调地址验证失败」:端点没有回显 challenge,或返回了非 200; 先用 curl 自测(见下)。
- 收不到事件:在应用设置里看「待投递 N · 失败 N」概况;失败通常是端点 5xx/超时。修好端点后点**「重投失败事件」**,已终态失败的投递会重新排队 (成功过的不重发)。
- 密钥轮换后:旧签名验不过是预期——新事件用新 secret 签名,接收端记得同步。
bash
# 本地自测回调端点
curl -s http://localhost:3000/jlt/events \
-H 'Content-Type: application/json' -H 'X-Jlt-Timestamp: 1700000000' -H 'X-Jlt-Signature: x' \
-d '{"type":"url_verification","challenge":"abc"}'
# → {"challenge":"abc"}