> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajuapi.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 웹훅 등록

<Info>
  **v1 Enterprise API (Coming Soon)**

  이 엔드포인트는 Enterprise 버전에서 제공될 예정입니다.
  현재는 [v0 API](/api-reference/v0/overview)를 사용하세요.
</Info>

웹훅을 등록하여 특정 이벤트 발생 시 알림을 받습니다. 등록된 웹훅은 지정된 이벤트가 발생할 때마다 HTTP POST 요청을 전송합니다.

<Note>
  웹훅 URL은 HTTPS를 사용해야 하며, 공개적으로 접근 가능해야 합니다. 웹훅 요청은 HMAC-SHA256으로 서명됩니다.
</Note>

***

## Request Body 파라미터

<ParamField body="url" type="string" required>
  웹훅 이벤트를 수신할 URL입니다. HTTPS만 지원됩니다. 최대 2048자입니다.
</ParamField>

<ParamField body="events" type="array" required>
  구독할 이벤트 목록입니다. 최소 1개 이상의 이벤트를 지정해야 합니다.
</ParamField>

<ParamField body="description" type="string">
  웹훅에 대한 설명입니다. 최대 200자입니다.
</ParamField>

<ParamField body="metadata" type="object">
  웹훅에 첨부할 메타데이터입니다. 키-값 쌍 형태입니다.
</ParamField>

***

## 지원 이벤트

| 이벤트                 | 설명            |
| ------------------- | ------------- |
| `profile.created`   | 새 프로필이 생성됨    |
| `profile.updated`   | 프로필이 수정됨      |
| `profile.deleted`   | 프로필이 삭제됨      |
| `fortune.generated` | 운세가 생성됨       |
| `fortune.cached`    | 캐시된 운세가 반환됨   |
| `fortune.deleted`   | 운세가 삭제됨       |
| `batch.completed`   | 배치 작업이 완료됨    |
| `daily.reset`       | 자정(KST) 일일 리셋 |
| `unmasked.accessed` | 복호화 엔드포인트 접근  |

***

## Response

### 성공

웹훅 등록에 성공하면 Webhook 객체가 반환됩니다. `secret` 필드는 생성 시에만 반환됩니다.

### 실패

| 상태 코드 | 에러 타입                  | 설명              |
| ----- | ---------------------- | --------------- |
| 400   | `validation_error`     | 요청 데이터가 유효하지 않음 |
| 401   | `authentication_error` | API 키가 유효하지 않음  |
| 429   | `rate_limited`         | 요청 한도 초과        |

***

## 요청 예시

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.sajuapi.dev/v1/webhooks \
    -H "X-API-Key: bs_live_xxx" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-server.com/webhooks/saju",
      "events": ["fortune.generated", "profile.created", "daily.reset"],
      "description": "운세 생성 알림",
      "metadata": {
        "environment": "production",
        "team": "backend"
      }
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.sajuapi.dev/v1/webhooks', {
    method: 'POST',
    headers: {
      'X-API-Key': 'bs_live_xxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://your-server.com/webhooks/saju',
      events: ['fortune.generated', 'profile.created', 'daily.reset'],
      description: '운세 생성 알림',
      metadata: {
        environment: 'production',
        team: 'backend'
      }
    })
  });

  const webhook = await response.json();
  // 중요: secret을 안전하게 저장하세요!
  console.log('Webhook Secret:', webhook.secret);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.sajuapi.dev/v1/webhooks',
      headers={'X-API-Key': 'bs_live_xxx'},
      json={
          'url': 'https://your-server.com/webhooks/saju',
          'events': ['fortune.generated', 'profile.created', 'daily.reset'],
          'description': '운세 생성 알림',
          'metadata': {
              'environment': 'production',
              'team': 'backend'
          }
      }
  )

  webhook = response.json()
  # 중요: secret을 안전하게 저장하세요!
  print('Webhook Secret:', webhook['secret'])
  ```
</CodeGroup>

***

## 응답 예시

```json theme={null}
{
  "id": "whk_abc123def456",
  "url": "https://your-server.com/webhooks/saju",
  "events": ["fortune.generated", "profile.created", "daily.reset"],
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "description": "운세 생성 알림",
  "metadata": {
    "environment": "production",
    "team": "backend"
  },
  "active": true,
  "failure_count": 0,
  "created_at": "2025-01-16T09:00:00Z",
  "updated_at": "2025-01-16T09:00:00Z"
}
```

***

## Webhook 객체

| 필드                  | 타입      | 설명                            |
| ------------------- | ------- | ----------------------------- |
| `id`                | string  | 웹훅 ID입니다. `whk_` 접두사로 시작합니다.  |
| `url`               | string  | 웹훅 수신 URL입니다.                 |
| `events`            | array   | 구독 중인 이벤트 목록입니다.              |
| `secret`            | string  | 서명 검증용 시크릿입니다. 생성 시에만 반환됩니다.  |
| `description`       | string  | 웹훅 설명입니다.                     |
| `metadata`          | object  | 메타데이터입니다.                     |
| `active`            | boolean | 활성화 상태입니다.                    |
| `failure_count`     | integer | 연속 실패 횟수입니다. 5회 초과 시 비활성화됩니다. |
| `last_triggered_at` | string  | 마지막 트리거 시간입니다.                |
| `created_at`        | string  | 생성 시간입니다.                     |
| `updated_at`        | string  | 수정 시간입니다.                     |

<Warning>
  `secret`은 웹훅 생성 시에만 반환됩니다. 이 값을 안전하게 저장하세요. 분실 시 웹훅을 삭제하고 다시 생성해야 합니다.
</Warning>

***

## 웹훅 페이로드

웹훅 이벤트가 발생하면 다음 형식의 페이로드가 POST 요청으로 전송됩니다.

```json theme={null}
{
  "id": "evt_xyz789",
  "type": "fortune.generated",
  "created_at": "2025-01-16T09:00:00Z",
  "data": {
    "fortune_id": "ftn_abc123",
    "profile_id": "prf_def456",
    "score": 85,
    "model": "sonnet",
    "cached": false
  }
}
```

***

## 서명 검증

모든 웹훅 요청에는 `X-Bithumb-Signature` 헤더가 포함됩니다. 이 서명을 검증하여 요청의 진위를 확인하세요.

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js 예시
app.post('/webhooks/saju', (req, res) => {
  const signature = req.headers['x-saju-signature'];
  const isValid = verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 이벤트 처리
  const { type, data } = req.body;
  console.log(`Received ${type}:`, data);

  res.status(200).json({ received: true });
});
```
