Webhooks
排队事件发生时,Webhook 会向您的系统发送 HTTP POST 请求。例如,顾客加入排队、工作人员叫号或完成服务时都可以触发请求。
- 打开 admin.jonot.io/settings/integrations。
- 点击添加端点。
- 输入您的服务器监听的公共 HTTPS URL。
- 选择要订阅的事件。
- 点击保存。
签名密钥出现时,请立即复制并妥善保存。Jonot 只显示一次。您可以在端点编辑页面点击发送测试请求,检查 Jonot 能否访问端点。
| 事件名称 | 触发时机 |
|---|---|
ticket.joined | 顾客加入排队 |
ticket.called | 员工将排队号叫到服务台 |
ticket.completed | 排队号被标记为完成 |
ticket.cancelled | 顾客或员工取消排队号,或管理员删除排队号所在的门店或队列 |
ticket.skipped | 排队号被跳过(可召回的暂缓) |
ticket.no_show | 已叫号或被跳过的排队号被确认为未到场 |
queue.status_changed | 队列状态变更(ACTIVE、PAUSED 或 CLOSED) |
管理员删除门店或队列时,Jonot 会为每个仍在等待或已被叫号的排队号发送 ticket.cancelled,每次删除最多 50 个排队号。超出上限的排队号不会产生事件。删除组织不会发送任何 Webhook,因为其端点会随组织一起删除。
每次投递都是一个 HTTP POST,带有:
Content-Type: application/jsonX-Jonot-Event: ticket.calledX-Jonot-Delivery-Id: <uuid>X-Jonot-Timestamp: 2024-06-01T12:00:00.000ZX-Jonot-Signature: v1,<base64-hmac-sha256>X-Jonot-Payload-Version: v1默认正文是 JSON 对象。payload 字段的内容取决于事件:
ticket.joined
{ "deliveryId": "<uuid>", "event": "ticket.joined", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "queueId": "q_…" }, "queue": { "id": "q_…", "name": "Main Queue" }, "location": { "id": "loc_…", "name": "Downtown" } }}ticket.called / ticket.completed / ticket.skipped / ticket.no_show
{ "deliveryId": "<uuid>", "event": "ticket.called", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "number": 42, "status": "CALLED", "queueId": "q_…", "createdAt": "2024-06-01T11:58:00.000Z", "updatedAt": "2024-06-01T12:00:00.000Z" } }}ticket.cancelled
{ "deliveryId": "<uuid>", "event": "ticket.cancelled", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticketId": "tkt_…", "queueId": "q_…" }}queue.status_changed
{ "deliveryId": "<uuid>", "event": "queue.status_changed", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "queueId": "q_…", "status": "PAUSED" }}Jonot 对以下字符串使用 HMAC-SHA256 为每次投递签名:
<deliveryId>.<timestamp>.<body>在 Node.js(≥18)中验证:
import { createHmac, timingSafeEqual } from "node:crypto";
/** * Returns true when the signature header is valid and the timestamp is * within 5 minutes of now. Throws for malformed input. */function verifySignature(secret, deliveryId, timestamp, body, header) { // Replay-attack guard: reject deliveries older than 5 minutes. const ageMs = Date.now() - new Date(timestamp).getTime(); if (Math.abs(ageMs) > 5 * 60 * 1000) return false;
const expected = "v1," + createHmac("sha256", secret) .update(`${deliveryId}.${timestamp}.${body}`) .digest("base64");
// timingSafeEqual prevents timing-oracle attacks. // Buffers must be the same length — if lengths differ the signature is // invalid, but we still compare a dummy value to keep constant time. const expectedBuf = Buffer.from(expected); const headerBuf = Buffer.from(header); if (expectedBuf.length !== headerBuf.length) return false; return timingSafeEqual(expectedBuf, headerBuf);}您可以将默认 JSON 正文替换为自定义模板。模板使用 Mustache 精简语法:
{{ path.to.value }}— 被替换并按内容类型转义:application/json为 JSON 字符串转义,application/x-www-form-urlencoded为百分号编码,text/plain为原样{{{ path.to.value }}}— 原样替换(不转义)
application/json 的模板示例(订阅了 ticket.joined):
{ "type": "{{ event }}", "ticketId": "{{ payload.ticket.id }}", "queueName": "{{ payload.queue.name }}"}载荷模板字段是代码编辑器,提供:
- 针对 JSON 模板的语法高亮和括号匹配。
- 变量列表 — 为所选事件显示可用变量路径。点击按钮即可在光标处插入
{{ path }}令牌。 - 实时验证 — 输入时,编辑器会检查变量路径。如果内容类型是
application/json,且模板不包含原始{{{ }}}标签,还会检查 JSON 结构。问题会在代码中标记,并列在编辑器下方:- 错误 — 在您选择的所有事件中都不存在的路径。
- 警告 — 仅存在于部分所选事件中的路径(其他事件的投递中将为空)。
验证在保存时也在服务器端运行 — 编辑器反馈与服务器规则完全一致。
端点返回非 2xx 状态或出现连接错误时,Cloudflare Queues 最多重试 3 次,每次重试前等待更长时间。如果初次投递和 3 次重试都失败,投递会进入死信队列,端点的连续失败次数加一。
端点累计20 次连续失败后自动停用。从端点编辑页面重新启用;计数器重置为零。
如果某个事件的投递会超出组织上限,该事件的全部投递都会记录为超出速率限制,且一条都不会发送。Jonot 不会重试,也不会计入自动停用端点的 20 次连续失败。投递记录标签页按每分钟上限记录被丢弃的投递,因此长时间的流量高峰只会留下有限数量的记录,而不是每条被丢弃的投递一条记录。
每个端点的投递记录标签页显示最近 30 天的投递尝试:事件类型、HTTP 状态、尝试次数和时间戳。使用加载更多按钮翻页查看更早的记录。
| 限制 | 值 |
|---|---|
| 每个组织的端点数 | 5 |
| 每个端点的自定义请求头数 | 10 |
| 请求头值长度 | 1 024 字节 |
| 载荷模板大小 | 16 KB |
| 投递超时 | 10 秒 |
| 投递历史保留期 | 30 天 |
| 每个组织的最大投递数 | 600 次 / 60 秒 |
| 每个组织记录的最大丢弃数 | 100 次 / 60 秒 |
轮换签名密钥
Section titled “轮换签名密钥”- 打开端点编辑页面。
- 点击轮换签名密钥。
- 在对话框中确认轮换。
- 立即复制新密钥 — 它只显示一次,无法找回。如果没保存就关闭对话框,您必须再次轮换才能获得新的明文。
- 更新您的服务器,用新密钥验证签名。
新旧密钥不能同时使用。 确认轮换后,旧密钥立即失效。请马上更新接收 Webhook 的系统。
UI 在轮换按钮旁边显示 当前密钥:····XXXX(最后四个字符),让您确认当前生效的是哪个密钥 — 在轮换后确认接收方和服务器同步时很有用。
保护签名密钥
Section titled “保护签名密钥”何时轮换:
- 疑似泄露:密钥出现在日志文件中、与离职员工共享过,或被屏幕录制捕获。
- 定期轮换:定期更换密钥可以缩短泄露密钥的可用时间。
- 任何能读取密钥的服务发生变更后(密钥管理轮换)。
如何更新接收方:
- 在管理 UI 中轮换并复制新密钥。
- 更新接收方密钥存储中的密钥(环境变量、密钥管理器等)。
- 部署更新后的接收方。
- 在投递记录标签页确认下一次投递成功。
如果在保存前丢失了新密钥:
再次轮换。每次轮换生成新的随机密钥。之前的明文无法找回 — 服务器端只存储 AEAD 加密形式。