本文旨在协助负责实现 Webhook 接收端的工程师,重点说明如何验证 X-Kanorio-Signature 签名、如何执行幂等去重,以及如何利用 cacheTags 触发外部前端缓存更新。
collectionKeys 或表单 formIds 过滤)或查看投递记录,请参阅Webhook 设置与管理。data 字段规格(SSOT):所有可订阅事件的代码、所需权限与 data JSON 示例,均由系统事件注册表自动生成于 Webhooks 参考页。每则 Webhook 通知皆以 HTTPS POST 发送,Content-Type 为 application/json,并附带以下 Headers:
| 头部 | 说明 |
|---|---|
X-Kanorio-Event | 事件代码(例如 collections.item.published、ec.order.paid),与 JSON 顶层的 event 字段相同 |
X-Kanorio-Delivery | 通知唯一编号,与 JSON 顶层的 id 字段相同;自动重试与后台手动“重发”时均保持不变 |
X-Kanorio-Signature | HMAC-SHA256 签名,格式为 t=时间戳,v1=签名;签名密钥轮替的 24 小时并行期内会带有两个 v1= |
User-Agent | Kanorio-Notifications/1.0 |
2xx:
接收端返回任何 2xx(如 200 OK、202 Accepted)即视为投递成功。若后续处理(如同步外部数据库或触发长耗时工作)较慢,请在验签与写入队列后先立即返回 200,再于后台异步处理。3xx 重定向:
Kanorio 发送 Webhook 时不会跟随任何 3xx 重定向(收到 301、302、307、308 一律视为投递失败)。请确认设置在后台的地址即为最终端点(留意结尾斜线 / 是否会触发框架自动跳转)。429 或 5xx 会自动进入指数退避重试;410 Gone 会立即停用该端点;连续失败超过上限时系统会自动暂停端点并通知团队。X-Kanorio-Delivery 做幂等去重:
网络重试或后台手动重送时,同一则通知可能送达超过一次。请以 X-Kanorio-Delivery(或 JSON 顶层的 id) 作为唯一键去重;若已处理过,直接返回 200 OK。X-Kanorio-Signature)自建接收端时,请务必使用后台该端点的“签名密钥”(whsec_...)验证 X-Kanorio-Signature,确认请求来自 Kanorio 且内容未被篡改:
X-Kanorio-Signature 以逗号 , 分割,取出 t= 后方的 Unix 秒数时间戳,以及所有 v1= 后方的十六进制签名字符串(密钥轮替的 24 小时并行期内会有两个 v1)。t 与服务器当前时间,若相差超过 5 分钟(300 秒) 请直接拒绝(每次重试都会以当前时间重新计算 t 与签名)。t、英文句点 . 与未经解析的原始请求字符串(Raw Request Body)串接为 {t}.{rawBody}。whsec_... 密钥对 {t}.{rawBody} 计算 HMAC-SHA256 十六进制摘要,并以 crypto.timingSafeEqual 比对是否与任一个 v1 相符。import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyKanorioWebhook(rawBody, signatureHeader, secret) { if (!signatureHeader || !secret) return false; const parts = signatureHeader.split(",").map((part) => part.split("=")); const t = parts.find(([key]) => key === "t")?.[1]; const signatures = parts .filter(([key]) => key === "v1") .map(([, value]) => value) .filter(Boolean); if (!t || signatures.length === 0) return false; const timestampSec = Number(t); if (!Number.isFinite(timestampSec) || Math.abs(Date.now() / 1000 - timestampSec) > 300) { return false; } const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const expectedBuf = Buffer.from(expected, "utf8"); return signatures.some((v1) => { const v1Buf = Buffer.from(v1, "utf8"); return v1Buf.length === expectedBuf.length && timingSafeEqual(expectedBuf, v1Buf); }); }
cacheTags 缓存更新所有 Webhook 事件皆采用相同的顶层 JSON 外壳,事件专属字段放在 data 内:
{ "version": "1", "id": "3f9c2a8b1e4d7c0f9a2b3c4d5e6f7a8b", "event": "collections.item.published", "occurredAt": "2026-10-21T08:30:00.000Z", "websiteId": "cmk1examplewebsiteid", "title": "Collection item published: Sourdough Guide", "summary": "blog/sourdough-guide (published)", "data": { "collectionId": "col_01", "collectionKey": "blog", "itemId": "item_01", "slug": "sourdough-guide", "status": "published", "publicVersion": "1729500000000", "cacheTags": ["site:cmk1examplewebsiteid", "collection:blog", "item:item_01"] }, "actionUrl": "https://app.kanorio.com/data/blog" }
data.cacheTags 做按需缓存更新(ISR)所有内容与集合变更事件(collections.item.*、collections.items.bulkChanged、collections.schema.changed、content.page.*、content.site.published)的 data 都会附上 publicVersion 与 cacheTags(对齐 Content Delivery API 响应 Header 的 X-Kanorio-Cache-Tags):
content.page.updated 会额外附上 previousSlug 并将 redirects 加入 cacheTags,让您的外部前端同时失效旧路径与重定向缓存。payload.data.cacheTags 逐一调用 revalidateTag(tag),即可在数秒内精准更新受影响的页面(完整示例见)。请直接前往由系统事件注册表自动生成的 Webhooks 参考页,上面列有全部可订阅事件的触发时机、过滤器支持与 data 示例。