# Webhook

> Kanorio에서 서명된 이벤트 알림을 수신하고, 검증하며, 각 전달을 한 번만 처리하고 프론트엔드 캐시를 갱신하는 방법을 알아봅니다.

> Markdown version of https://kanorio.com/ko/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/ko/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` 값과 상수 시간(constant time) 비교를 수행합니다. `t`가 현재 시간과 5분 이상 차이 나는 요청은 거부하십시오. JSON 파싱 전의 원본 요청 본문을 반드시 사용해야 합니다.

서명 시크릿을 교체하는 동안 Kanorio는 24 hours 동안 이전 시크릿과 새 시크릿 모두로 서명하므로, 헤더에 두 개의 `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`를 구독하거나, 더 나은 방법으로 각 페치(fetch)에 응답 자체의 태그를 지정하십시오.

Next.js에서는 `createRevalidateHandler`를 사용하여 서명 검증과 태그 재검증을 한 줄로 처리할 수 있습니다. 모든 태그 앞에 `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을 호스트의 배포 훅(deploy hook)으로 지정하고 콘텐츠 이벤트를 구독하십시오.
