# Webhook

> Kanorioから署名付きイベント通知を受け取り、検証し、各配信を一度だけ処理してフロントエンドのキャッシュを更新する方法について説明します。

> Markdown version of https://kanorio.com/ja/developers/webhooks. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp
>
> Translated from the English original: https://kanorio.com/en/developers/webhooks.md

Webhookは、サイトで何らかのイベント（投稿の公開、フォームの送信、注文の支払いなど）が発生した際に、システムへ通知を送る仕組みです。Kanorioは、イベントを購読している各エンドポイントに対して、JSON形式のボディを含むHTTPS `POST`リクエストを送信します。

## エンドポイントの追加

エンドポイントは[サイト設定 › Webhook](https://app.kanorio.com/settings/webhooks)から追加するか、Management APIの `POST /sites/{id}/webhooks` を使用して追加できます。各エンドポイントで以下を設定します。

- **イベント:** [イベントリファレンス](https://kanorio.com/en/developers/webhooks/events)には、製品ごとにグループ化されたすべてのイベントと、必要な権限が記載されています。
- **フィルタ:** コレクションイベントは特定のコレクションに、フォームイベントは特定のフォームに限定できます。
- **署名シークレット:** 各エンドポイントには、署名を検証するための固有の `whsec_` シークレットが割り当てられます。

サイトごとのエンドポイント数はプランによって異なります。[レート制限とクォータ](https://kanorio.com/ja/developers/basics/rate-limits)をご確認ください。

## リクエスト

| ヘッダー | 値 |
|---|---|
| `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` 内に格納されます。

```json
{
  "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` | `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を計算し、各 `v1` 値と定数時間で比較します。`t` が現在時刻から5分以上経過しているリクエストは拒否してください。JSON解析を行う前の、生の（raw）リクエストボディを必ず使用してください。

署名シークレットをローテーションする際、Kanorioは 24 hours の間、新旧両方のシークレットで署名を行うため、ヘッダーには2つの `v1` 値が含まれます。いずれかが一致すればリクエストを承認してください。

SDKを使用すると、この処理を自動化できます。

```ts
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での実装例は以下の通りです。

```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` を使用すると、署名の検証とタグの再検証を1行で行うことができます。すべてのタグに `kanorio:` というプレフィックスが付与されるため、Kanorioのタグと独自のタグが競合することはありません。`frameworkCacheTags` または `cacheTag` ビルダーを通じてフェッチにタグを付与し、両側で一致するようにしてください（生のタグを使用する場合は `tagPrefix: ""` を渡します）。

```ts
// 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をホストのデプロイフックに向け、コンテンツイベントを購読してください。
