Webhook
把一个端点订阅到工作区事件、我们会 POST 什么,以及今天怎么安全地接收它。
Webhook 订阅会在工作区里发生某件事时向你的端点 POST,让另一个系统不用轮询就能做出反应。
订阅
设置 → Webhook → 新建订阅:一个事件加一个 https:// 端点。一个订阅对应一个事件——要订多个事件就订阅多次。
端点在你保存时就会被校验,而不是等到我们第一次调用:它必须是一个格式正确的公网 https:// URL。内网地址和回环地址会被拒绝,这条拒绝是一个安全控制,不是格式检查。
事件
| 事件 | 何时触发 |
|---|---|
conversation.created | 会话被创建 |
conversation.updated | 状态、优先级或分配人变化 |
message.created | 消息发出或收到 |
message.status_changed | 出站消息被送达、已读或失败 |
order.created | 订单被创建 |
order.verified | 订单被核销 |
order.refunded | 订单被退款 |
insight.emitted | 营收智能产出一条洞察 |
我们发送什么
POST,Content-Type: application/json:
{
"event": "message.created",
"payload": { "…": "这个事件所关于的对象" },
"timestamp": "2026-08-21T09:14:22.000Z",
"delivery_id": "…"
}带上这些 header:
| Header | 含义 |
|---|---|
X-Juno-Event | 事件名,让你不解析 body 就能路由 |
X-Juno-Signature | sha256=<hex>——对 body 原始字节做的 HMAC-SHA256 |
X-Juno-Delivery-Id | 同一事件的重试之间保持不变——你的去重键 |
投递是至少一次的。 同一个 delivery_id 可能到达不止一次,所以把你的处理逻辑做成幂等的:记下这个 id,重复的就忽略。手动重投(重新投递按钮)不带 delivery_id,因为它不是任何东西的重试。
快速返回 2xx。 其他任何响应都会被记为该订阅的 last status 并显示在它那一行——集成安静下来时,那里是第一个该看的地方。
安全地接收它——在你依据 payload 做事之前先读这段
投递是签了名的,但签名密钥目前没有任何地方能读到:它在服务端按工作区派生,没有任何界面或接口会返回它。在这一点补上之前,你无法校验 X-Juno-Signature,而一个依据未校验的 POST 做事的端点,会依据任何人的 POST 做事。
所以,今天:
- 把 body 当成一条通知,不要当成数据。 从里面取出对象的 id,再用 API 密钥通过 API 把对象读回来。那条路径是有鉴权的,而且顺带避免了依据一个已经被后续变更覆盖掉的 payload 做事。
- 让端点难以猜到,并加上限流。
- 绝不让它仅凭这个 POST 就执行任何破坏性或涉及资金的操作。
这是一个真实的限制,不是文档的疏漏;把它明明白白写在这里,是因为另一种做法——描述一套你实际上执行不了的签名校验——会让你以为一个端点受到了保护,而它并没有。