Kanorio

API 参考

Webhooks

接收来自 Kanorio 的已签署事件通知,验证请求,确保每个传送仅处理一次,并刷新您的前端缓存。

Webhooks 会在网站发生特定事件时通知您的系统:例如发布文章、提交表单或支付订单。Kanorio 会向订阅该事件的每个端点发送包含 JSON 内容的 HTTPS POST 请求。

新增端点

您可以在 网站设置 › Webhooks 新增端点,或通过 Management API 使用 POST /sites/{id}/webhooks。针对每个端点,您可以选择:

  • 事件 (Events)。 事件参考 列出了所有事件,并按产品分组,同时标注所需的权限。
  • 过滤器 (Filters)。 集合事件可限制为特定集合,表单事件则可限制为特定表单。
  • 签名密钥 (Signing secret)。 每个端点都有专属的 whsec_ 密钥,用于验证签名。

每个网站的端点数量取决于您的计划。请参阅 速率限制与配额。

请求 (The request)

标头值
Content-Typeapplication/json
X-Kanorio-Event事件键值,例如 collections.item.published
X-Kanorio-Delivery传递 ID。在重试或手动重新发布时保持不变。
X-Kanorio-Signaturet=<unix seconds>,v1=<hex signature>
User-AgentKanorio-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,并订阅内容事件即可。