---
title: 웹훅 검증
description: verifyWebhook은 Sume 전달의 sume-v1 서명을 확인합니다. 원본 본문, 헤더 형태, 재전송 허용 시간, 그리고 왜 async인지 살펴보세요.
---

Sume는 모든 웹훅 전달에 `<timestamp>.<raw_body>`에 대한 HMAC-SHA256 서명을
붙여 `sume-v1=<hex>` 형태로 보냅니다. `verifyWebhook`이 그 확인을 대신하므로
직접 구현하지 않아도 됩니다.

```ts
import { verifyWebhook } from "@sume-com/sdk";

export async function POST(request: Request) {
  const body = await request.text(); // JSON.parse 이전의 원본

  const ok = await verifyWebhook({
    body,
    headers: request.headers,
    // Sume 전달 워커가 서명할 때 쓰는 것과 같은 이름입니다. Sume가 발급하며 아직 셀프서브가 아닙니다.
    secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET!,
  });
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = JSON.parse(body);
  await recordTerminalRun(event.request_id, event); // request_id로 중복 제거
  return new Response(null, { status: 204 }); // 빠르게 2xx, 작업은 그다음에
}
```

서명 시크릿은 대시보드의 **웹훅** 탭(`/dashboard/webhooks` — 표시 후 복사)에서
직접 확인하거나, `account:read` 범위를 가진 API 키로
`GET /v1/webhooks/signing-secret`을 호출해 받을 수 있습니다. 워크스페이스별로
파생된 값이므로, 서명이 유효하다는 것은 공용 시크릿을 가진 누군가가 아니라
여러분에게 서명되었다는 뜻입니다. 로컬 샘플이 Sume 워커의 서명과 맞도록 환경변수
이름은 `SUME_COM_WEBHOOK_SIGNING_SECRET`을 쓰고, API 키와 같은 수준으로
보관하세요. API 키와는 다른 값이며 클라이언트와도 무관합니다.
`verifyWebhook`은 `client`를 받지 않고 요청도 보내지 않습니다.

대시보드에는 시크릿의 **지문**도 함께 표시되고, 모든 전송에
`x-sume-webhook-secret-fingerprint` 헤더로 같은 값이 실립니다. 서명 검증이 계속
실패하면 지문을 비교하세요. 이 과정에서 티켓에 붙여넣어도 안전한 값은 지문뿐입니다.

## 시크릿 교체

시크릿이 유출되었을 수 있다면 교체하세요. 대시보드의 **웹훅 → 시크릿 교체**,
또는 `account:write` 범위를 가진 키로 `POST /v1/webhooks/signing-secret/rotate`를
호출하면 됩니다.

교체는 즉시 전환이 아닙니다. 이후 **24시간** 동안 Sume는 모든 전송을 두 시크릿
모두로 서명하고, `x-sume-webhook-signature` 헤더에 새 서명부터 순서대로 쉼표로
이어 보냅니다.

```http
x-sume-webhook-signature: sume-v1=<새 서명>,sume-v1=<이전 서명>
```

둘 중 하나만 갖고 있어도 검증되므로, 버튼을 누른 순간에 맞춰 배포할 필요 없이
편한 일정으로 수신 서버를 교체하면 됩니다. 이 기간이 지나면 이전 시크릿은 더 이상
검증되지 않습니다. 기간이 열려 있는 동안 대시보드에 만료 시각이 표시되고, 두 API
응답 모두 `rotation.previous_valid_until`로 같은 값을 전달합니다.

<Callout type="warn">
  `verifyWebhook`이 다중 서명 헤더를 처리하는 것은 **`@sume-com/sdk` 0.5.0**
  부터입니다. 그보다 낮은 버전이나 헤더를 문자열 일치로 비교하는 자체 구현은 이
  기간 동안 모든 전송에서 실패합니다. 교체하기 **전에** 수신 서버를 먼저 올려
  주세요. 기간이 아닐 때는 서명이 하나만 실리므로, 교체하지 않는 분들에게는 아무
  변화가 없습니다.
</Callout>

`x-sume-webhook-secret-fingerprint`는 교체한 순간부터 이 기간 중에도 **새** 시크릿을
가리킵니다. 지금 어떤 시크릿이 함께 허용되는지가 아니라, 어떤 시크릿으로 옮겨가야
하는지를 알려 주는 값입니다. 한 기간 안에서 두 번 교체하면 두 단계 전 시크릿은 즉시
폐기되며, 유출이 실제로 멈추는 지점이 바로 이 동작입니다.

## 입력

| 필드 | 설명 |
|---|---|
| `body` | **원본** 본문입니다. `string`, `ArrayBuffer`, 또는 타입 배열입니다. |
| `headers` | `Headers`, `Map`, 또는 일반 객체(Node의 `req.headers`)입니다. 대소문자를 구분하지 않습니다. |
| `secret` | Sume 웹훅 서명 시크릿입니다. |
| `toleranceSeconds` | 재전송 허용 시간입니다. 기본값은 `300`입니다. `0`이면 타임스탬프 확인을 건너뜁니다. |

읽는 헤더는 두 개입니다.

```text
x-sume-webhook-timestamp: 1785000000
x-sume-webhook-signature: sume-v1=<hex_signature>
```

## 동작을 좌우하는 네 가지 규칙

- **원본 본문을 넘기세요.** 파싱했다가 다시 직렬화한 객체는 검증되지 않습니다.
  키 순서와 공백도 서명 대상의 일부입니다. JSON을 대신 파싱해 주는 프레임워크는
  이미 바이트를 망가뜨린 상태입니다. Express에서는 웹훅 라우트에만
  `express.raw({ type: "application/json" })`을 붙이세요. Next.js App
  Router에서는 무엇보다 먼저 `await request.text()`를 호출하세요.
- **`async`입니다.** 구현이 `node:crypto` 대신 WebCrypto를 쓰기 때문에 Workers,
  Deno, 그리고 `node:` 스펙파이어를 거부하는 번들러에서도 패키지를 import할 수
  있습니다. `await` 하세요.
- **잘못된 전달에는 throw하지 않고 `false`를 반환합니다.** 헤더 누락, 엉뚱한
  타임스탬프, 잘못된 서명은 모두 그냥 검증 실패입니다. 분기할 지점은 하나이고
  `try`/`catch`는 필요 없습니다.
- **비교는 상수 시간으로** 이뤄지고, 재전송 허용 시간은 HMAC을 계산하기 전에
  먼저 확인합니다.

## 검증기 하나, 표면 둘

Run 웹훅(`*.run.terminal`, `run_id` 포함)과 생성 Job 웹훅(`job.*`, `job_id`
포함)은 `sume-v1` 스킴을 그대로 공유합니다. 페이로드는 다르지만 서명은
같습니다. 그래서 검증기 하나로 둘 다 처리할 수 있습니다. **`event`로
분기하고**, 본문에 `run_id`가 있다고 가정하지 마세요.

```ts
const event = JSON.parse(body);

switch (event.event) {
  case "format.run.terminal":
    return handleFormatRun(event);
  case "job.completed":
  case "job.failed":
    return handleJob(event);
  default:
    return new Response(null, { status: 204 }); // 모르는 이벤트, 500이 아님
}
```

알 수 없는 이벤트를 `204`로 처리하는 것이, 새로 추가된 이벤트 타입이 500과
재시도 폭주로 번지는 것을 막아 줍니다.

## 필요하다면 쓰는 상수

`verifyWebhook`이 일반적인 경우를 처리합니다. 수신기를 직접 통제하지 못할
때 — 예를 들어 게이트웨이가 코드 실행 전에 검증하는 경우 — 를 위해 구성
요소도 내보냅니다.

| Export | 값 |
|---|---|
| `SUME_WEBHOOK_SIGNATURE_VERSION` | `"sume-v1"` |
| `SUME_WEBHOOK_SIGNATURE_HEADER` | `"x-sume-webhook-signature"` |
| `SUME_WEBHOOK_TIMESTAMP_HEADER` | `"x-sume-webhook-timestamp"` |
| `DEFAULT_SUME_WEBHOOK_TOLERANCE_SECONDS` | `300` |

## 다음

- [Run 웹훅](/agents/run-webhooks) — 전달, 이벤트, 페이로드, 재시도, 그리고
  JavaScript가 아닌 수신기를 위한 원본 스킴입니다
- [웹훅](/workflows/webhooks) — 다른 표면인 생성 Job 웹훅입니다
- [실행 기다리기](/sdk/runs) — 전달이 아직 꺼져 있을 때 쓰는 폴링 경로입니다
