本文旨在協助負責實作 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 範例。