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

# 알림 webhook: 팀 알림을 엔드포인트로 전송

> Didit에서 검토, AML 모니터링 결과, 결제, webhook 실패, 상태 등 팀 알림을 직접 운영하는 HTTPS 엔드포인트로 전송합니다. HMAC-SHA256 서명, 24시간 재시도.



알림 webhook 프롬프트
        프롬프트를 클립보드에 복사



      
      

\<pre>

\`Add an endpoint that receives Didit alert webhooks (team alerts, not verification results).

\## What to build
A public HTTPS POST endpoint (e.g. /api/didit/alerts) that:
1\. Reads the raw request body as bytes (do NOT parse and re-serialize the JSON before verifying)
2\. Checks that X-Timestamp is within 300 seconds of now
3\. Recomputes X-Signature-V2 = hex(HMAC-SHA256(secret, X-Timestamp + "." + rawBody)), where secret is the FULL signing secret including its "whsec_" prefix
4\. Compares the two signatures in constant time
5\. Parses the JSON, ignores an alert_id it already handled, answers 2xx within 5 seconds, then processes the alert asynchronously
6\. Answers a failed check with 400, not 401 (401, 403, 404 and 410 mark the destination for reconnection)

\## Headers
\- Content-Type: application/json
\- User-Agent: Didit-Alerts/1.0
\- X-Timestamp: Unix seconds, new on every attempt
\- X-Signature-V2: hex HMAC-SHA256 described above
This is NOT the signature scheme of verification webhooks. Alert webhooks send no X-Signature and no X-Signature-Simple.

\## Body (compact JSON, keys sorted)
\{
"alert_id": "alt_<16 hex characters>",  // the same on every retry: deduplicate on it
"created_at": 1791884460,                // Unix seconds when Didit created the alert
"data": { "event": "session_in_review", "label": "Session", "value": "#1042" },
"organization": { "id": "<uuid>", "name": "<your organization>" },
"severity": "info" | "warning" | "critical",
"subtitle": "<English explanation, can be empty>",
"title": "<English headline>",
"url": "<link to the Didit console>",
"webhook_type": "alert.<type>"
}

\## webhook_type values
\- alert.verification.in_review, alert.business_verification.in_review
\- alert.transaction.held, alert.aml.monitoring_hit
\- alert.billing.low_balance, alert.billing.autorefill_failed (data.paused), alert.billing.backup_card_charged
\- alert.webhook.delivery_failing
\- alert.service.degraded (data.status "degraded" or "down"), alert.service.recovered (data.status "recovered")
\- alert.test (sent from the console's Send test button)
Ignore webhook_type values and data keys you do not recognize.

\## Retries
\- 2xx = delivered. Redirects are not followed.
\- Timeout (5 s), connection failure, 5xx, 408, 425 or 429 = retried 5 times, after 1 min, 5 min, 30 min, 3 h and 20 h 24 min (about 24 h).
\- Any other response is final.
\- Test alerts are never retried.

\## Secret rotation
Rotating the secret in the console replaces it at once. There is no overlap and no header signed with the previous secret: deploy the new secret immediately.

Docs: /integration/alert-webhooks`

\</pre>



      
      

붙여넣을 곳

알림 webhook은 팀의 알림을 여러분이 소유한 HTTPS endpoint로 전송합니다.
온콜 도구, 티켓 대기열 또는 자체 운영 대시보드에 알림을 전달하는 데 사용하세요.알림 webhook은 [인증 webhook](https://docs.didit.me/ko/integration/webhooks.md)과 별개입니다.
인증 webhook은 세션, 엔티티 및 거래 결과를 애플리케이션으로 전송합니다.
알림 webhook은 검토가 필요한 인증, 실패한 결제, 서비스 장애처럼 주의가 필요한 상황을 팀에 알립니다.
알림 webhook에는 고유한 페이로드, 서명 비밀, 서명 및 재시도 일정이 있습니다.## 대상 설정알림 webhook은 API가 아니라 Business Console에서 구성합니다.
조직 설정을 관리할 수 있는 멤버가 webhook 대상을 추가, 변경 및 테스트할 수 있습니다.**1. 대상 추가****조직 설정** > **팀 알림**에서 endpoint URL을 입력하여 **알림 webhook** 대상을 추가합니다.
URL은 `https://`를 사용해야 하며 공개 주소로 해석되어야 합니다.**2. 서명 비밀 저장**Didit은 `whsec_`로 시작하는 전체 서명 비밀을 한 번만 표시합니다.
지금 저장하세요. 이후 콘솔에는 마지막 네 자리만 표시됩니다.**3. 알림 라우팅**대상을 추가해도 아직 아무것도 전송되지 않습니다.
수신하려는 알림의 라우팅 규칙에 대상을 추가하세요.**4. 테스트 전송****테스트 전송**을 클릭하여 `alert.test` 알림을 보내고, endpoint가 이를 수락하는지 확인하세요.조직은 **팀 알림**에 최대 25개의 webhook 대상을 가질 수 있습니다.프로덕션 활동만 webhook 대상으로 전송됩니다.
샌드박스 인증, 기업 확인(KYB), 거래 및 AML 히트에 대한 알림은 대상으로 절대 전송되지 않습니다.## 알림 유형| `webhook_type` | `severity` | Didit이 전송하는 시점 |
| - | - | - |
| `alert.verification.in_review` | `warning` | 인증이 완료되어 수동 검토 대상으로 표시되었습니다. |
| `alert.business_verification.in_review` | `warning` | 기업 확인(KYB)에 수동 검토가 필요합니다. |
| `alert.transaction.held` | `warning` | 거래 모니터링 규칙이 거래를 검토 대상으로 표시했습니다. |
| `alert.aml.monitoring_hit` | `warning` | 지속적인 AML 모니터링에서 새로운 AML 히트가 발견되었습니다. |
| `alert.billing.low_balance` | `warning` | 조직의 잔액이 설정한 임계값 아래로 떨어졌습니다. |
| `alert.billing.autorefill_failed` | `critical` | 카드가 거부되어 잔액의 자동 충전이 실패했습니다. |
| `alert.billing.backup_card_charged` | `warning` | 한 카드에서 자동 결제가 거부되어 백업 카드로 결제되었습니다. |
| `alert.webhook.delivery_failing` | `warning` | 하나의 [인증 webhook](https://docs.didit.me/ko/integration/webhooks.md) endpoint로의 전송이 실패하고 있습니다. |
| `alert.service.degraded` | `critical` | 조직이 사용하는 서비스의 상태가 저하되었거나 중단되었습니다. |
| `alert.service.recovered` | `info` | 해당 서비스가 정상으로 복구되었습니다. |
| `alert.test` | `info` | 누군가 webhook 대상에서 **테스트 전송**을 클릭했습니다. |## 페이로드모든 알림은 JSON 본문을 가진 `POST` 요청입니다.
Didit은 키를 정렬한 압축된 JSON으로 본문을 전송합니다.
이 예시는 읽기 쉽도록 포맷되어 있습니다.```json theme={null}
{
  "alert_id": "alt_9f2c4e1a7b3d5c80",
  "created_at": 1791884460,
  "data": {
    "event": "session_in_review",
    "label": "Session",
    "value": "#1042"
  },
  "organization": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "Example Org"
  },
  "severity": "warning",
  "subtitle": "A KYC or KYB verification was completed and flagged for manual review.",
  "title": "Verification in review",
  "url": "https://business.didit.me/console/3fa85f64-5717-4562-b3fc-2c963f66afa6/9b2d1c4e-8f3a-4e6b-a1d2-5c7e9f0b3a14/verifications?session-id=1b4e28ba-2fa1-41d2-883f-0016d3cca427",
  "webhook_type": "alert.verification.in_review"
}
```| 필드 | 유형 | 설명 |
| - | - | - |
| `webhook_type` | string | `alert.`에 알림 유형을 붙인 값입니다. [알림 유형](#알림-유형)을 참조하세요. |
| `alert_id` | string | `alt_`에 16자리 16진수 문자를 붙인 값입니다. 알림의 모든 재시도에는 같은 `alert_id`가 포함되므로 이를 사용해 중복을 무시하세요. |
| `created_at` | integer | Didit이 알림을 생성한 Unix 초 단위 시간입니다. 재시도 시에도 동일하게 유지됩니다. |
| `severity` | string | `info`, `warning` 또는 `critical`입니다. |
| `organization` | object | 조직의 `id`(UUID)와 `name`입니다. |
| `title` | string | 짧은 영문 제목입니다. |
| `subtitle` | string | 영문 설명입니다. 비어 있을 수 있습니다. |
| `data` | object | 알림의 대상에 대한 정보입니다. 아래를 참조하세요. |
| `url` | string | Didit 콘솔의 관련 페이지 링크입니다. |`data`에는 항상 다음이 포함됩니다.- `event`: 이벤트 키입니다. 예: `session_in_review`.
- `label`과 `value`: 알림의 대상을 나타냅니다. 예: `Session`과 `#1042`.일부 알림 유형은 `data`에 키를 추가합니다.- `alert.billing.autorefill_failed`는 `paused`를 추가합니다. 이는 카드를 업데이트할 때까지 자동 충전이 일시 중지되었는지를 나타내는 불리언 값으로, 일시 중지된 경우 `true`입니다.
- `alert.service.degraded`와 `alert.service.recovered`는 `service`, `status`, `incident_id`, `service_name`, `state`, `affected`, `country_name`, `started_at`, `resolved_at`, `duration_minutes`, `title`, `body`를 추가합니다.
  `status`는 `alert.service.degraded`에서 `degraded` 또는 `down`이고, `alert.service.recovered`에서는 `recovered`입니다.
  `service`는 [`GET /v3/status/`](https://docs.didit.me/ko/api-reference/service-status)의 서비스 키를 사용하며, `incident_id`는 그곳의 인시던트 `id`와 일치합니다.알림에는 최종 사용자의 개인 정보나 문서 데이터가 절대 포함되지 않습니다.
세션, 거래 또는 기타 항목은 번호로 식별되며, 콘솔의 해당 항목으로 연결되는 링크가 제공됩니다.
인식하지 못하는 `webhook_type` 값과 `data` 키는 무시하세요. 그래야 새로운 값이나 키가 추가되어도 endpoint가 중단되지 않습니다.## 요청 헤더| 헤더 | 예시 | 의미 |
| - | - | - |
| `Content-Type` | `application/json` | 본문은 JSON입니다. |
| `User-Agent` | `Didit-Alerts/1.0` | Didit의 알림 발신자를 식별합니다. |
| `X-Timestamp` | `1791884462` | Didit이 이 시도에 서명한 Unix 초 단위 시간입니다. 시도할 때마다 새로 생성됩니다. |
| `X-Signature-V2` | 64자리 16진수 문자 | 이 시도의 HMAC-SHA256 서명입니다. |## Verify the signature`X-Signature-V2`는 `<X-Timestamp>.<raw body>` 문자열에 대해 대상의 서명용 시크릿을 키로 하여 계산한 HMAC-SHA256 값을 16진수로 인코딩한 것입니다.* Didit이 표시한 전체 시크릿을 `whsec_` 접두사를 포함하여 그대로 사용하십시오.
* 요청을 받은 그대로의 원본 바이트에 대해 서명을 검증하십시오. JSON을 파싱했다가 다시 직렬화하면 바이트가 바뀌어 검증이 실패할 수 있습니다.
* 서명을 비교할 때는 상수 시간 비교를 사용하십시오.
* `X-Timestamp`가 현재 시각과 5분을 초과하여 차이 나는 요청은 거부하십시오. Didit은 재시도를 포함한 모든 전송 시도마다 새 타임스탬프로 서명하므로, 이 허용 범위 때문에 정상적인 재시도가 거부되는 일은 없습니다.
* 검증에 실패하면 `400`으로 응답하십시오. `401`, `403`, `404` 또는 `410` 응답은 대상을 다시 연결해야 하는 상태로 표시합니다([재시도](#재시도) 참조).> [!WARNING]
>
> 알림 webhook은 검증 webhook과 같은 방식으로 서명되지 않습니다.
> 헤더 이름도 `X-Signature-V2`이지만, 서명 대상 문자열이 다릅니다. 또한 알림 webhook은 `X-Signature` 또는 `X-Signature-Simple` 헤더를 보내지 않습니다.
> 검증 webhook용 검증기를 재사용하지 마십시오.### Node.js (Express)```javascript theme={null}
const express = require("express");
const crypto = require("crypto");

const app = express();
// The full secret Didit showed when you added the destination, including "whsec_".
const ALERT_SECRET = process.env.DIDIT_ALERT_SECRET;
const TOLERANCE_SECONDS = 300;

// express.raw keeps the body as the exact bytes Didit sent.
app.post("/api/didit/alerts", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Timestamp") || "";
  const signature = Buffer.from(req.get("X-Signature-V2") || "", "utf8");

  const now = Math.floor(Date.now() / 1000);
  if (!/^\d+$/.test(timestamp) || Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) {
    return res.status(400).send("Stale or missing timestamp");
  }

  const expected = Buffer.from(
    crypto.createHmac("sha256", ALERT_SECRET).update(`${timestamp}.`).update(req.body).digest("hex"),
    "utf8",
  );
  if (signature.length !== expected.length || !crypto.timingSafeEqual(signature, expected)) {
    return res.status(400).send("Invalid signature");
  }

  const alert = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(204); // answer within 5 seconds, then process
  handleAlert(alert); // skip an alert_id you already handled
});
```### Python (Flask)```python theme={null}
import hashlib
import hmac
import json
import os
import time

from flask import Flask, request

app = Flask(__name__)
# The full secret Didit showed when you added the destination, including "whsec_".
ALERT_SECRET = os.environ["DIDIT_ALERT_SECRET"].encode("utf-8")
TOLERANCE_SECONDS = 300


@app.post("/api/didit/alerts")
def didit_alert():
    raw_body = request.get_data()  # the exact bytes Didit sent
    timestamp = request.headers.get("X-Timestamp", "")
    signature = request.headers.get("X-Signature-V2", "")

    try:
        too_old = abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS
    except ValueError:
        too_old = True
    if too_old:
        return "Stale or missing timestamp", 400

    expected = hmac.new(ALERT_SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected.encode(), signature.encode()):
        return "Invalid signature", 400

    alert = json.loads(raw_body)
    handle_alert(alert)  # skip an alert_id you already handled; keep this fast
    return "", 204
```### 서명 시크릿 교체**팀 알림**에서 대상의 시크릿을 교체합니다.
Didit은 새 시크릿을 한 번만 표시하며, 교체 이후의 모든 전송 시도에 이 시크릿으로 서명합니다. 교체 이전에 생성된 알림의 재시도도 포함됩니다.
이전 시크릿은 즉시 사용할 수 없게 됩니다. 겹치는 기간은 없으며, 이전 시크릿으로 만든 서명이 담긴 헤더도 더 이상 전송되지 않습니다.
시크릿을 교체하면 바로 endpoint에 새 시크릿을 배포하십시오.## 재시도Didit은 `2xx` 응답을 전송 완료로 간주합니다.
각 시도는 응답을 최대 5초 동안 기다리며, 리다이렉트를 따르지 않습니다.Didit은 시간 초과, 연결 실패, `5xx` 응답, `408`, `425` 또는 `429` 응답이 발생하면 알림을 재시도합니다.
재시도는 최대 5회이며, 약 24시간에 걸쳐 진행됩니다.| 재시도 | 이전 시도 이후 지연 시간 |
| - | - |
| 1차 | 1분 |
| 2차 | 5분 |
| 3차 | 30분 |
| 4차 | 3시간 |
| 5차 | 20시간 24분 |그 밖의 모든 응답은 최종 응답이며, `3xx`와 그 밖의 `4xx` 응답도 포함됩니다.
**테스트 전송**으로 보낸 요청은 한 번만 시도되며, 재시도하지 않습니다.`401`, `403`, `404` 또는 `410` 응답도 대상을 다시 연결해야 하는 상태로 표시합니다. 콘솔에는 **다시 연결** 상태로 나타납니다.
Didit이 전송할 때 URL이 사설 주소나 그 밖의 차단된 주소로 확인되는 경우에도 같은 상태가 됩니다.
Didit은 해당 대상으로 새 알림을 계속 전송하며, 다음 전송이 성공하거나 URL을 업데이트하면 다시 활성 상태가 됩니다.**팀 알림**의 **최근 알림**에서는 각 전송과 그 결과를 확인할 수 있으며, 실패한 전송은 여기서 다시 시도할 수 있습니다.## 관련 문서- [검증 webhook](https://docs.didit.me/ko/integration/webhooks.md): 애플리케이션의 세션, 엔터티 및 거래 결과를 다룹니다.
- [서비스 상태](https://docs.didit.me/ko/api-reference/service-status.md): `alert.service.degraded` 및 `alert.service.recovered` 이벤트의 기반이 되는 공개 피드입니다.
- [수동 검토](https://docs.didit.me/ko/console/manual-review.md): 검토 중인 인증을 처리하는 방법입니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.