API 参考
Webhooks
接收来自 Kanorio 的已签署事件通知,验证请求,确保每个传送仅处理一次,并刷新您的前端缓存。
Webhooks 会在网站发生特定事件时通知您的系统:例如发布文章、提交表单或支付订单。Kanorio 会向订阅该事件的每个端点发送包含 JSON 内容的 HTTPS POST 请求。
新增端点
您可以在 网站设置 › Webhooks 新增端点,或通过 Management API 使用 POST /sites/{id}/webhooks。针对每个端点,您可以选择:
- 事件 (Events)。 事件参考 列出了所有事件,并按产品分组,同时标注所需的权限。
- 过滤器 (Filters)。 集合事件可限制为特定集合,表单事件则可限制为特定表单。
- 签名密钥 (Signing secret)。 每个端点都有专属的
whsec_密钥,用于验证签名。
每个网站的端点数量取决于您的计划。请参阅 速率限制与配额。
请求 (The request)
| 标头 | 值 |
|---|---|
Content-Type | application/json |
X-Kanorio-Event | 事件键值,例如 collections.item.published |
X-Kanorio-Delivery | 传递 ID。在重试或手动重新发布时保持不变。 |
X-Kanorio-Signature | t=<unix seconds>,v1=<hex signature> |
User-Agent | Kanorio-Notifications/1.0 |
每个事件都使用相同的封装格式。事件特定的字段位于 data 中。
{ "version": "1", "id": "3f9c2a8b1e4d7c0f9a2b3c4d5e6f7a8b", "event": "collections.item.published", "occurredAt": "2026-11-02T08:30:00.000Z", "websiteId": "ws_example123", "title": "Collection item published: Sourdough Guide", "summary": "blog/sourdough-guide (published)", "data": { "collectionKey": "blog", "itemId": "item_01", "slug": "sourdough-guide", "status": "published", "publicVersion": "1729500000000", "cacheTags": ["collection:blog", "item:item_01", "seo", "search"] }, "actionUrl": "https://app.kanorio.com/data/blog", "operationId": null, "principal": { "kind": "USER", "id": "usr_example01" } }
| 字段 | 备注 |
|---|---|
id | 传递 ID,等于 X-Kanorio-Delivery。请使用此 ID 进行去重。 |
event | 等于 X-Kanorio-Event。 |
data | 事件特定内容。内容事件包含 locale、publicVersion 和 cacheTags。 |
operationId | 针对属于追踪操作的事件进行设置,例如表单提交、支付、退款和访问授权。请使用 GET /operations/{id} 查询。否则为 null。 |
principal | 发起变更的对象:USER、SERVICE_ACCOUNT、CUSTOMER、SYSTEM 或 VISITOR。未知时为 null;切勿将 null 视为上述任何一种。 |
验证签名
使用端点的密钥计算 {t}.{rawBody} 的 HMAC-SHA256,并以恒定时间 (constant time) 与每个 v1 值进行比较。若请求中的 t 与您的系统时间相差超过五分钟,请拒绝该请求。请务必使用原始请求内容 (raw request body),在进行任何 JSON 解析之前进行验证。
当您轮替签名密钥时,Kanorio 会在 24 hours 期间同时使用新旧密钥进行签署,因此标头会包含两个 v1 值。只要其中一个相符,即可接受请求。
SDK 会为您处理此过程:
import { verifyWebhookSignature } from "@kanorio/sdk"; export async function POST(request: Request) { const rawBody = await request.text(); const valid = await verifyWebhookSignature({ rawBody, signatureHeader: request.headers.get("x-kanorio-signature"), signingSecret: process.env.KANORIO_WEBHOOK_SECRET!, }); if (!valid) return new Response("Invalid signature", { status: 401 }); const event = JSON.parse(rawBody); // handle event.event and event.data return new Response(null, { status: 200 }); }
若不使用 SDK,在 Node.js 中:
import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyKanorioWebhook(rawBody, header, secret) { if (!header || !secret) return false; const parts = header.split(",").map((part) => part.split("=")); const t = parts.find(([key]) => key === "t")?.[1]; const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value); if (!t || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")); return signatures.some((v1) => { const actual = Buffer.from(v1); return actual.length === expected.length && timingSafeEqual(actual, expected); }); }
响应与重试
- 请在 8 seconds 内回传任何
2xx状态码。若处理时间较长,请先将事件放入队列并立即响应。 - Kanorio 不会跟随重定向。
3xx会被视为失败,因此请注册最终的 URL,包含您的框架所预期的结尾斜线。 - 超时、网络错误、
429与5xx错误将在 1m, 5m, 30m, 2h, 8h, 1d, 1d 后重试 7 次。每次重试都会使用新的时间戳重新签署。 410 Gone会立即禁用该端点。若连续失败 3 次,端点将会暂停,并通知网站团队。- 传送内容会保留 7 days,因此您可以在此期间从后台重新发送传送记录。
确保每个传送仅处理一次
重试与手动重新发送可能会导致同一个事件被传送多次。请存储您已处理的 X-Kanorio-Delivery ID,当您再次看到相同的 ID 时,请直接回传 200 而无需重复执行作业。
刷新前端缓存
每个 Content Delivery API 的响应都会在 X-Kanorio-Cache-Tags 中列出其依赖项目,而每个内容事件也会在 data.cacheTags 中列出变更项目,并使用相同的标签。请使用响应标签标记您框架的缓存,并重新验证事件标签,这样所有使用该变更内容的页面将在几秒钟内刷新,而其他内容则保持缓存状态。
| 标签 | 响应范围 | 触发事件 |
|---|---|---|
site:<id> | 网站的每项响应 | content.site.published, localization.locale.*(语言切换器和 hreflang 存在于每个页面) |
locale:<code> | 次要语言的所有响应 | localization.job.completed |
page:<id>, item:<id>, product:<id>, form:<id> | 该记录在所有语言中的响应,以及嵌入该记录的响应(面包屑导航祖先、展开的引用) | 记录本身的事件,因此主要语言的变更会刷新所有语言(未翻译的字段会回退至主要文本) |
page:<id>@<code>, item:<id>@<code>, … | 仅该语言的响应 | content.translation.published / deleted:仅刷新该语言 |
pages, collection:<key>, products (以及 @<code>) | 仅列表响应 | 该列表中记录的事件 |
collection:<key>:items | 该集合的每项单一项目响应 | collections.schema.changed,以及过大而无法逐项列出的批量变更 |
globals, header:<key>, footer | 页眉和页脚响应 | content.site.published,全局区段翻译 |
search, seo, redirects | 网站搜索、SEO 文件 (/seo/*)、重定向表 | 变更这些项目的内容事件;content.page.updated 会在页面重命名时新增 redirects |
标签使用语言代码,而非 URL 前缀(拥有者可能会变更前缀)。revalidateTag 会匹配完整字符串,因此事件总是列出具体的标签。单笔记录事件不会携带 site:<id>:如果您仅使用网站标签来标记缓存,请订阅 content.site.published,或者更好的做法是,使用响应本身的标签来标记每次抓取。
在 Next.js 中,createRevalidateHandler 可在单行代码内完成签名验证与标签重新验证。它会为每个标签加上 kanorio: 前缀,以避免 Kanorio 的标签与您的标签冲突;请通过 frameworkCacheTags 或 cacheTag 构建器来标记您的抓取内容,确保双方匹配(传入 tagPrefix: "" 即可使用原始标签):
// app/api/revalidate/route.ts import { revalidateTag } from "next/cache"; import { createRevalidateHandler } from "@kanorio/sdk"; export const POST = createRevalidateHandler({ signingSecret: process.env.KANORIO_WEBHOOK_SECRET!, revalidateTag: (tag) => revalidateTag(tag), });
对于静态构建的网站,请将 Webhook 指向您主机的部署 Hook,并订阅内容事件即可。