
# 변경 로그에 웹훅 구독하기

`POST https://api.keenauth.com/v1/accounts/{account_id}/changelog-subscriptions` · 계정 소유자 또는 관리자

선택한 카테고리 중 하나에서 [변경 로그](/docs/changelog) 항목이 게시되면 `changelog.entry_published` 웹훅을 받을 고객님의 HTTPS 엔드포인트를 등록합니다. 요청은 `keenauth login`의 자격 증명으로 보내며, 로그인한 계정이어야 하고 해당 계정의 소유자 또는 관리자여야 합니다. 계정당 활성 구독은 최대 5개까지 가질 수 있습니다.

각 구독에는 고유한 서명 비밀이 있으며 응답에서 한 번만 반환됩니다. 전달은 Standard Webhooks를 따릅니다. 모든 요청에는 `webhook-id`, `webhook-timestamp`, `webhook-signature` 헤더가 포함되며, 서명(`v1,<base64>`)은 `<webhook-id>.<webhook-timestamp>.<body>`에 대한 HMAC-SHA256입니다. HMAC 키는 `idp_whsec_` 접두사 뒤의 43자가 인코딩한 32바이트입니다(base62, big-endian). 마지막 6자는 체크섬이며 키의 일부가 아닙니다. 본문을 신뢰하기 전에 서명을 검증하고, 현재 시각과 너무 차이 나는 타임스탬프는 거부하십시오(Standard Webhooks 라이브러리는 5분을 허용합니다).

URL에 개인 정보, 자격 증명 또는 토큰을 넣지 마십시오. URL은 구독과 함께 저장되며 계정의 모든 소유자와 관리자에게 표시됩니다.

## 요청

헤더:

| 헤더 | 필수 | 설명 |
| --- | --- | --- |
| `Authorization` | 예 | `DPoP <access token>`: CLI가 로그인할 때 받은 액세스 토큰입니다. |
| `DPoP` | 예 | 이 요청에 대한 DPoP 증명(`htm` `POST`, `htu`는 이 요청의 URL)입니다. 액세스 토큰이 바인딩된 키로 서명하며, 액세스 토큰의 해시와 서버 nonce를 포함합니다. |
| `Content-Type` | 예 | `application/json`. |
| `Idempotency-Key` | 아니요 | 16~64자의 `A-Z`, `a-z`, `0-9`, `_` 및 `-` 문자입니다. 같은 키와 본문으로 24시간 이내에 요청을 반복하면 비밀이 없는 같은 구독이 `secret_redacted: true` 및 `Idempotent-Replayed: true` 헤더와 함께 반환됩니다. |

본문:

| 필드 | 필수 | 설명 |
| --- | --- | --- |
| `url` | 예 | 엔드포인트입니다: `https`, 포트 443, 호스트 이름(IP 주소나 `.localhost`, `.local`, `.internal` 같은 로컬 전용 이름은 불가), 사용자 정보나 프래그먼트 없음, 최대 2048바이트. |
| `categories` | 아니요 | `api`, `sdk`, `deprecation`, `security`, `incident` 중 하나이며, 각각 최대 한 번만 사용할 수 있습니다. 필드를 생략하면 모든 카테고리를 받습니다. 빈 배열은 거부됩니다. |

## 응답

`201 Created` 응답(`Cache-Control: no-store` 포함):

| 필드 | 설명 |
| --- | --- |
| `id` | 구독(`chlg_…`)입니다. |
| `account_id` | 계정(`acct_…`)입니다. |
| `url` | 엔드포인트입니다. |
| `categories` | 받을 카테고리로, `api`, `sdk`, `deprecation`, `security`, `incident` 순서입니다. |
| `status` | `enabled`이며, 전달이 계속 실패한 경우에는 `disabled`입니다. |
| `created_at` | 구독이 생성된 시각(RFC 3339, UTC)입니다. |
| `signing_secret` | 서명 비밀(`idp_whsec_…`)입니다. 이 응답에서만 반환되므로 비밀 관리 도구에 저장하십시오. |
| `secret_redacted` | 멱등 재요청일 때는 `true`이며, 이 경우 `signing_secret`은 포함되지 않습니다. |

## 오류

| 상태 | 코드 | 발생 시점 |
| --- | --- | --- |
| 400 | `malformed_request` | 본문이 JSON이 아니거나 `Idempotency-Key` 헤더가 반복되었거나, 비어 있거나, 형식이 잘못된 경우입니다. |
| 400 | `unknown_field` | 본문에 `url`와 `categories` 외의 필드가 있는 경우입니다. |
| 401 | `unauthenticated` | 액세스 토큰이 전송되지 않은 경우입니다. |
| 401 | `invalid_token` | 액세스 토큰이 유효하지 않거나 만료되었거나 CLI의 토큰이 아닙니다. `keenauth login`으로 다시 로그인하십시오. |
| 401 | `invalid_dpop_proof` | DPoP 증명이 없거나 반복되었거나, 요청 또는 토큰 키와 일치하지 않습니다. |
| 401 | `use_dpop_nonce` | `DPoP-Nonce` 응답 헤더의 nonce를 포함한 증명으로 다시 시도하십시오. |
| 401 | `insufficient_user_authentication` | 다중 인증이 필요합니다(소유자와 관리자의 경우 패스키 또는 보안 키). 다시 로그인하십시오. |
| 403 | `insufficient_permissions` | 계정의 소유자 또는 관리자가 아닌 경우입니다. |
| 403 | `account_restricted` | 계정 상태상 변경이 허용되지 않는 경우입니다. |
| 403 | `quota_exceeded` | 계정에 이미 활성 구독이 5개 있습니다(`errors[].code` `changelog_subscription_limit_exceeded`). |
| 404 | `not_found` | 계정이 없거나, 로그인한 계정이 아니거나, 해당 계정의 구성원이 아닌 경우입니다. |
| 413 | `payload_too_large` | 본문이 8 KiB를 초과하는 경우입니다. |
| 415 | `unsupported_media_type` | 본문이 `application/json`가 아닌 경우입니다. |
| 422 | `validation_failed` | `url`이 없거나 잘못되었거나, 카테고리를 알 수 없거나 반복되었거나, `categories`가 비어 있거나, 필드 타입이 잘못된 경우입니다. |
| 422 | `idempotency_key_reuse` | `Idempotency-Key`는 24시간 이내에 다른 본문과 함께 사용되었습니다. |
| 429 | `rate_limited` | 최근 1시간 동안 계정에 구독이 10개를 초과해 생성되었습니다. `Retry-After`까지 기다리십시오. |

## 웹훅 전달

항목이 게시되면 KeenAuth는 항목의 카테고리 중 하나를 포함하는 모든 활성 구독의 URL로 `POST`을 하나씩 보냅니다. 같은 항목이 한 구독에 두 번 전송되지는 않습니다.

### 요청

| 헤더 | 설명 |
| --- | --- |
| `Content-Type` | `application/json`. |
| `webhook-id` | 이벤트 `id`(`evt_…`)입니다. 같은 항목의 모든 재시도에서 동일하므로 중복을 무시하는 데 사용하십시오. |
| `webhook-timestamp` | 이 시도에 서명한 Unix 시간(초)입니다. 시도마다 새로 설정됩니다. |
| `webhook-signature` | `v1,<base64>`: 위에서 설명한 HMAC-SHA256이며, `<webhook-id>.<webhook-timestamp>.<body>`에 대한 서명입니다. |

본문은 최대 16 KiB입니다. KeenAuth는 리디렉션을 따르지 않습니다.

### 이벤트

```json
{
  "id": "evt_01jb0000000000000000000000",
  "type": "changelog.entry_published",
  "schema_version": 1,
  "occurred_at": "2026-11-04T00:00:00Z",
  "recorded_at": "2026-11-04T08:30:12Z",
  "data": {
    "id": "2026-11-04-example",
    "url": "https://docs.keenauth.com/en/changelog/#2026-11-04-example",
    "title": "Example entry",
    "date_published": "2026-11-04T00:00:00Z",
    "tags": ["deprecation"],
    "categories": ["api", "deprecation"],
    "surfaces": [{"kind": "api", "name": "management-v1", "version": "v1"}],
    "breaking": false,
    "sunset_at": "2027-11-04"
  }
}
```

| 필드 | 설명 |
| --- | --- |
| `id` | 이벤트(`evt_…`)로, `webhook-id` 헤더와 같습니다. |
| `type` | 항상 `changelog.entry_published`입니다. |
| `schema_version` | 항상 `1`입니다. 버전 1 안에서 `data`에 새 선택 필드가 추가될 수 있으므로, 알지 못하는 필드는 무시하십시오. |
| `occurred_at` | 항목이 게시된 시각(RFC 3339)입니다. |
| `recorded_at` | KeenAuth가 전달을 큐에 넣은 시각(RFC 3339)입니다. |
| `data.id`, `data.url`, `data.title`, `data.date_published` | 변경 로그 피드와 같은 항목입니다. |
| `data.tags` | `added`, `changed`, `fixed`, `removed`, `deprecation`, `security`, `breaking`, `incident` 중 하나 이상입니다. |
| `data.categories` | 항목이 속한 구독 카테고리입니다. |
| `data.surfaces` | 영향을 받는 표면입니다: `kind`(`api`, `sdk`, `cli`, `provider`, `mcp` 또는 `docs`), `name` 그리고 알 수 있는 경우 `version`. |
| `data.breaking` | 변경이 호환되지 않는 경우 `true`. |
| `data.sunset_at` | 지원 중단의 제거 날짜(`YYYY-MM-DD`)이며, 그 외에는 `null`입니다. |

기계 판독 가능한 스키마는 `changelog.entry_published.v1`입니다.

### 응답과 재시도

15초 이내에 `2xx` 상태 코드로 응답하십시오. 응답 본문은 무시됩니다. 그 밖의 결과는 실패한 시도로 간주되어 재시도됩니다. 연결 또는 TLS 오류, 시간 초과, 리디렉션, `429`, `5xx` 및 `410`을 제외한 그 밖의 모든 상태 코드가 여기에 포함됩니다. 첫 시도는 즉시 이루어지며, 이후 시도는 이전 실패 후 약 1분, 5분, 30분, 2시간, 8시간, 24시간, 36시간 뒤에 이루어집니다(각 간격은 최대 10퍼센트까지 변동). 총 약 3일 동안 8번 시도합니다.

여덟 번째 시도가 실패하면 전달은 포기되고 구독은 비활성화됩니다(`status` `disabled`). 이미 큐에 있던 전달은 취소됩니다.

### 410 Gone

KeenAuth에 더 이상 전달을 원하지 않음을 알리려면 `410 Gone`으로 응답하십시오. 구독은 추가 시도 없이 즉시 비활성화되며, 큐에 있던 전달도 취소됩니다. 비활성화된 구독은 다시 활성화되지 않으며 5개 한도에도 포함되지 않습니다. 다시 항목을 받으려면 새 구독을 만드십시오. 모든 비활성화는 계정 감사 로그에 `changelog.subscription_disabled`로 기록되며, 사유는 `receiver_gone`, `permanent_failure` 또는 `schedule_exhausted`입니다.

## 예제

```bash
curl -X POST https://api.keenauth.com/v1/accounts/acct_01jb0000000000000000000000/changelog-subscriptions \
  -H "Authorization: DPoP $ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: changelog-hooks-2026-10" \
  -d '{"url": "https://hooks.example.com/keenauth/changelog", "categories": ["deprecation", "security"]}'
```
