메인 콘텐츠로 건너뛰기
인증 활동과 릴리스에 따라 달라지는 웹훅 전송은 인증 이벤트 및 웹훅에 문서화되어 있습니다. 현재 웹훅 대상 API에는 목록에 없는 인증 이벤트 유형을 등록하지 마십시오.

개요

웹훅은 검증 세션, 기업 세션, 벤더 사용자, 벤더 기업 또는 거래의 상태가 변경될 때마다 Didit이 실시간 업데이트를 백엔드로 전송하는 방식입니다. 관심 있는 이벤트에 하나 이상의 웹훅 대상을 구독하면 결과가 준비되는 즉시 반응할 수 있으며, 보통 인증 중인 사용자가 절차를 마친 후 몇 초 이내입니다. 웹훅은 권장되는 통합 방식입니다. GET /v3/session/{id}/decision/을 통한 폴링도 대체 수단으로 지원되지만 속도가 느리고 요청을 더 많이 사용하며, data.updated, transaction.status.updated 및 웹훅으로만 전송되는 엔티티 수준의 변경 같은 이벤트를 건너뜁니다. 웹훅 전송을 정보의 기준으로 삼고, 콜드 스타트 시 또는 데이터 대조를 위해서만 폴링하십시오. 이 장은 Didit Academy 통합 강의의 웹훅 개요부터 이어집니다. Academy 통합 강의는 3:12부터 웹훅을 다룹니다. 웹훅 대상 추가, 이벤트 선택, 그리고 아래에서 검증에 사용할 서명용 비밀을 설명합니다.
Cloudflare / WAF 사용자. Didit은 정적 IP 18.203.201.92에서 User-Agent: DiditWebhook/2.0 +https://didit.me를 사용해 웹훅을 전송합니다. 엣지에서 알 수 없는 클라이언트를 차단하는 경우, Security > WAF > Tools > IP Access Rules에서 이 IP를 허용하고 수신 호스트 이름에 대해 허용을 선택하십시오.

이벤트 유형

대상의 subscribed_events 배열에는 정확한 값을 사용합니다. 와일드카드는 없으므로 받으려는 이벤트 계열을 각각 나열해야 하며, 이벤트를 여러 대상에 나누어 보낼 수 있습니다. 이 표에 있는 값이 구독 가능한 이벤트의 전체 목록입니다. session.status.updated나 kyc.completed처럼 표에 없는 이벤트는 없습니다. Didit이 보내는 다른 전송은 서명되지 않은 kyb.registry_search.resolved 콜백뿐이며, 이 콜백은 POST /v3/kyb/search/를 호출할 때 전달하는 요청별 webhook_url로 전송되고, webhook 대상으로는 전송되지 않습니다.
Business Console > API & Webhooks > Try Webhook 메뉴는 이러한 이벤트 각각에 대해 완성된 페이로드를 전송합니다(승인됨, 거부됨, 검토 중, 유니코드 이름, KYC/KYB, 엔티티, 활동, 거래 포함). 실제 서비스를 시작하기 전에 이 기능으로 엔드포인트를 검증하세요.

payload 구조

엔벨로프

모든 webhook은 동일한 엔벨로프를 공유합니다. 필드는 항상 sort_keys=True와 compact separators(, / :)로 직렬화되므로, 같은 payload는 양쪽에서 같은 서명을 재현합니다. 세션 이벤트(status.updated, 일반 세션과 기업 세션의 data.updated)에는 엔벨로프에 다음 필드가 추가로 포함됩니다.

세션 엔벨로프 예시

decision 객체는 GET /v3/session/{id}/decision/이 반환하는 것과 같은 스키마입니다. 기능별 속성의 전체 참조(id_verifications[], aml_screenings[] 등의 모든 속성)는 데이터 모델을 참조하세요. 스키마를 한 곳에서만 관리하도록 기능별 필드 표는 이 페이지에 의도적으로 반복하지 않았습니다.

V3 복수형 배열(중요)

decision 안의 모든 기능별 결과는 복수형 배열입니다. 따라서 하나의 워크플로에 같은 기능의 인스턴스가 여러 개 포함될 수 있습니다(예: 여러 문서에 걸친 여러 신분증 검사).
각 항목에는 워크플로 그래프와 일치하는 node_id와 기능별 status가 포함됩니다. 읽지 마세요: 레거시 V2 단수형 필드(id_verification, nfc, liveness, face_match, phone, email, poa, aml, ip_analysis, database_validation)는 webhook 대상이 webhook_version: "v2"로 명시적으로 고정된 경우에만 나타납니다. 전자 서명 결과는 signatures[]에 있으며, 워크플로에 전자 서명 단계가 있는 경우에만 존재합니다. 서명자마다 항목이 하나씩 있으며, 서명자, 코드 확인, 인증서, 타임스탬프, 각 문서의 해시와 다운로드 경로, 증빙 PDF가 포함됩니다. 별도의 서명 이벤트는 없습니다. status.updated가 결과를 읽어야 할 시점을 알려 줍니다. 전자 서명 보고서를 참조하세요. 은행 계좌 확인 결과는 bank_verifications[]에 있으며, 은행 계좌 확인 단계마다 항목이 하나씩 있습니다. 각 항목의 method는 계좌를 어떻게 확인했는지를 나타냅니다. open_banking(은행 로그인), account_lookup(계좌 확인), 또는 statement 또는 microdeposits 대체 방식이며, 계좌가 평가되기 전까지는 null입니다. 사용자가 Continue without your bank를 선택하면 continued_without_bank는 true이고, continuation에 그 이유가 담깁니다. 이 경우 method는 none이며, 이전에 시도가 평가된 적이 있는 경우에만 예외입니다. 사용자가 은행 계좌를 확인할 수 없는 경우를 참조하세요. 계좌 확인 방식으로 검증된 계좌는 금융 데이터를 읽지 않으므로, financial_data.not_collected에는 해당 단계가 요청했지만 수집하지 않은 잔액, 거래 내역, 소득 정보가 나열됩니다. 은행 계좌 확인 방식을 참조하세요.

Investigator 블록

사용자 또는 기업 세션의 V3 decision에는 investigator도 포함됩니다. 이 값은 세션의 최신 Investigator 검토 결과이며, 검토가 한 번도 없었다면 null입니다. 문서 수집 기능인 KYB 문서와 Document AI에는 별도의 복수형 배열이 없습니다. 대신 결정의 features[] 목록에 { "feature": "DOCUMENT_AI", "node_id": "…" } 형태로 나타나며, 노드당 항목 하나씩 들어갑니다. 자세한 내용은 Document AI를 참조하세요. 사용자 인증 결정에는 단일 biometric_consent 객체도 포함됩니다. 이 객체는 세션에서 명시적인 생체 인식 동의가 필요했는지, trigger, 응답의 status와 그 근거를 나타냅니다. status: "pending"이고 reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"이면, 4.10.0 미만의 네이티브 SDK가 동의 단계 없이 얼굴을 캡처했다는 뜻입니다. 자세한 내용은 생체 인식 동의를 참조하세요.

신분증 인증 방법 키

모든 id_verifications[] 항목에는 해당 항목을 생성한 신분증 인증 방법이 표시됩니다. 따라서 핸들러는 실제로 받은 근거에 따라 분기 처리할 수 있습니다. 기존 핸들러는 그대로 동작합니다. 문서 세션은 verification_method를 document로 설정하고 나머지 값은 null로 둡니다. 필드별 타입은 데이터 모델에서 확인할 수 있습니다.

승인된 지갑 세션 (status.updated)

엔티티 이벤트 payload

엔티티 상태는 ACTIVE, FLAGGED, BLOCKED 중 하나입니다. user.data.updated와 business.data.updated는 status 필드를 그대로 유지하면서, changed_fields 배열과 { field: { previous, current } } 형태의 changes 맵을 추가로 포함합니다. business.* 이벤트는 vendor_user_id 대신 vendor_business_id를 사용합니다.

거래 이벤트 payload

거래 webhook은 다른 webhook과 같은 팬아웃 인프라를 사용하고 동일한 envelope 형식을 사용합니다. 다만 세션 대신 거래를 식별합니다.
action_required는 null이거나, 대기 중인 사용자 작업입니다. 이 작업은 type(wallet_ownership 또는 verification_session)과 url로 표시되며, 필수 사용자 작업에 설명되어 있습니다. 새 거래의 기본 상태는 APPROVED입니다. 생성 과정에서 규칙, 차단 목록 또는 워크플로가 상태를 바꾸면, 최초의 transaction.created payload에는 그 최종 상태가 반영됩니다. transaction.status.updated는 같은 형태이며 업데이트된 status를 포함합니다. 거래 상태는 APPROVED, IN_REVIEW, DECLINED, AWAITING_USER입니다. severity는 UNKNOWN / LOW / MEDIUM / HIGH / CRITICAL이고, direction은 INBOUND / OUTBOUND입니다. transaction.created와 transaction.status.updated는 프로덕션 애플리케이션에 대해서만 전송됩니다. 샌드박스 Travel Rule 교환은 environment: sandbox와 함께 travel_rule.status.updated를 전송합니다.

KYB 레지스트리 검색 콜백

비동기 POST /v3/kyb/search/ 흐름은 webhook 대상 이벤트가 아닌 추가 payload를 하나 전달합니다. 검색 요청에 webhook_url을 전달하면, 레지스트리 조회가 끝난 뒤 Didit이 해당 URL로 kyb.registry_search.resolved 콜백을 POST합니다. 이 콜백은 webhook 대상과 세 가지 점에서 다릅니다.
  • 서명되지 않음입니다. X-Signature* 헤더가 없습니다. 대신 Didit은 X-Didit-Unsigned-Callback: true를 사용해 서명되지 않은 콜백임을 표시합니다. 검증하려면 request_id를 검색 요청이 반환한 ID와 비교하세요.
  • 이벤트 이름은 webhook_type이 아니라 event_type에 있습니다. application_id와 environment는 없습니다.
  • timestamp와 created_at은 epoch 정수입니다. 다른 webhook과 같은 형식이지만, 검색 엔드포인트의 ISO created_at과는 다릅니다.
이 콜백은 webhook 대상과 같은 재시도 정책을 따릅니다. 재시도할 때도 timestamp를 다시 찍고, 서명되지 않은 상태를 유지합니다. 후보 목록 스키마 전체는 KYB 레지스트리 API를 참조하세요.

서명 검증

모든 웹훅은 대상의 secret_shared_key를 사용하는 HMAC-SHA256으로 서명됩니다. 사용 중인 스택에서 잘 동작하는 헤더를 고를 수 있도록 세 가지 서명 헤더를 보냅니다. Didit이 항상 보내는 헤더는 다음과 같습니다. 대상의 서명 비밀이 교체된 후 24시간 동안에는 전송에 X-Signature-Previous, X-Signature-V2-Previous, X-Signature-Simple-Previous도 포함됩니다(서명 비밀 교체 참조).

서명 비밀 교체

대상의 서명 비밀은 Business Console에서 교체합니다. 교체 후 24시간 동안 해당 대상으로 가는 모든 전송에는 교체 전 비밀로 생성된 헤더 세 개가 추가로 포함됩니다. 일반 서명 헤더 또는 -Previous 헤더 중 어느 하나라도 일치하면 허용하는 핸들러는 새 비밀을 배포하는 동안에도 계속 검증에 성공합니다. 이전 비밀은 -Previous 헤더와, 새 비밀은 일반 헤더와 일치합니다. 아래 코드 샘플이 이 방식을 구현합니다. 24시간 안에 새 비밀을 배포하십시오. 그 이후에는 새 비밀로 서명된 요청만 검증되기 때문입니다. GET /v3/webhook/destinations/{destination_uuid}/의 previous_secret_expires_at에서 24시간이 언제 끝나는지 확인할 수 있습니다. 재시도와 재전송은 전송될 때 대상의 현재 비밀로 다시 서명되며, 24시간이 유지되는 동안에만 -Previous 헤더를 포함합니다. 24시간 안에 다시 교체하면 가장 오래된 비밀은 즉시 폐기됩니다. -Previous 헤더를 통해 검증되는 것은 새 교체 직전까지 현재였던 비밀뿐이며, 이 역시 새 교체 시점부터 24시간 동안만 유지됩니다. -Previous 헤더는 웹훅 대상에만 존재합니다. 기존 webhook_url에 서명하는 애플리케이션 수준의 secret_shared_key에는 중복 기간이 없습니다. PATCH /v3/webhook/의 rotate_secret_key로 교체하면 이전 비밀은 즉시 무효화됩니다.

X-Signature-Simple은 지원 중단됨

X-Signature-Simple은 payload가 아니라 엔벨로프를 인증합니다. 이 헤더의 정규 문자열은 timestamp, session_id, status, webhook_type만 포함합니다. decision은 포함하지 않으며, resubmit_info, metadata, vendor_data, event_id도 포함하지 않습니다. 이 네 필드를 그대로 둔 채 decision만 다시 쓴 요청도 X-Signature-Simple 검증을 통과합니다.session_id가 전혀 없는 이벤트에서는 보호 수준이 더 낮습니다. 거래 이벤트(transaction_id, txn_id, amount, score)와 엔티티 이벤트(vendor_user_id, vendor_business_id)는 "{timestamp}::{status}:{webhook_type}"에 서명하며, 이 문자열은 이벤트가 어떤 거래 또는 엔티티에 관한 것인지 식별하지 않습니다. 같은 초에 같은 상태로 보고된 서로 다른 두 거래는 동일한 X-Signature-Simple을 갖습니다. travel_rule.status.updated, workflow.compliance_changed, transaction_rule_suggestions.created 이벤트에도 status가 없으므로 "{timestamp}:::{webhook_type}"에 서명합니다. 예를 들어 "{timestamp}:::travel_rule.status.updated"와 같습니다.X-Signature-Simple만 확인한다면, 이 네 필드 외의 모든 내용을 신뢰할 수 없는 것으로 취급하고 API에서 레코드를 다시 가져오십시오. 세션은 GET /v3/session/{session_id}/decision/, 거래는 GET /v3/transactions/{transaction_id}/를 사용하십시오. 이 API가 기록의 신뢰할 수 있는 원본입니다.
이는 그 자리에서 패치할 수 있는 문제가 아니라 호환성 제약입니다. 네 필드로 이루어진 정규 문자열은 와이어 계약입니다. X-Signature-Simple을 검증하는 모든 수신 측은 정확히 그 네 필드를 다시 계산하므로, 이 헤더가 서명하는 대상을 바꾸면 서명 경로에서 이들 모두가 조용히 깨집니다. 본문까지 포함하려면 양쪽이 바이트 단위로 똑같이 재현할 수 있는 명시된 JSON 정규 형식도 필요하며, Didit은 이미 이를 X-Signature-V2로 제공하고 있습니다. 자체 정규화 규칙을 가진 본문 포함 변형을 하나 더 추가하는 대신, 이 헤더는 현재 그대로 유지하고 기존의 전체 payload 변형을 우선하여 이 헤더를 지원 중단했습니다. 이것이 사용자에게 의미하는 바는 다음과 같습니다.
  • 와이어에서는 아무것도 바뀌지 않습니다. X-Signature-Simple은 모든 전송에 같은 방식으로 계산되어 계속 포함됩니다. 기존 검증 로직은 계속 동작하며, 제거가 있을 경우 충분히 미리 알려드리겠습니다.
  • 새 연동은 X-Signature-V2를 사용해야 합니다. 미들웨어에 안전하며 동시에 전체 payload에 서명합니다.
  • X-Signature-Simple에서 벗어날 수 없는 경우에는 검증한 다음, 웹훅 본문에서 값을 읽지 말고 API에서 레코드를 가져오십시오.

세 가지 변형이 있는 이유

일부 웹 프레임워크(Express 본문 파서, Django 미들웨어, API 게이트웨이)는 핸들러가 읽기 전에 JSON을 조용히 다시 인코딩합니다. 원본 본문에 "José"가 있었는데 미들웨어가 이를 "José"로 다시 쓰면, 데이터는 동일하더라도 바이트가 바뀝니다. 그러면 X-Signature는 검증에 실패합니다. X-Signature-V2는 거의 모든 미들웨어가 결정적으로 재현하는 정규 JSON 형식(키 정렬, 압축 구분자, 이스케이프되지 않은 유니코드)으로 계산됩니다. 지원 중단된 X-Signature-Simple은 그마저 실패할 때 파서와 무관한 짧은 문자열에만 서명하는 방식으로 폴백합니다. 이 헤더는 그 짧은 문자열에만 서명하고 본문은 전혀 서명하지 않으므로, 본문을 보증할 수 없습니다.

권장 검증 순서

  1. X-Signature-V2를 먼저 시도합니다. JSON.parse(body)를 키 정렬과 유니코드 보존 방식으로 다시 인코딩한 다음, HMAC-SHA256(secret, canonical)을 계산하고 헤더 값과 timingSafeEqual로 비교합니다.
  2. V2 검증이 실패하고 원시 바이트를 읽을 수 있다면, HMAC-SHA256(secret, rawBody)에 대해 X-Signature를 시도합니다.
  3. 두 방법이 모두 실패한 경우에만 지원 중단된 X-Signature-Simple을 사용합니다. 이때 HMAC-SHA256(secret, "{timestamp}:{session_id}:{status}:{webhook_type}")와 비교합니다. 이 방식은 엔벨로프만 인증합니다. 본문의 decision은 신뢰할 수 없는 값으로 취급하고, 작업을 수행하기 전에 GET /v3/session/{session_id}/decision/에서 다시 가져오십시오.
각 단계에서 대응하는 -Previous 헤더가 있으면, 두 헤더 중 어느 쪽과 일치하더라도 승인합니다(서명 비밀 교체 참조). 모든 경우에 리플레이 공격을 막기 위해 5분보다 오래된 요청(abs(now - X-Timestamp) > 300)은 거부하십시오.

재시도 정책

endpoint가 5xx 또는 404로 응답하면, Didit은 지수 백오프를 적용하여 최대 2회 재시도합니다.
  • 첫 번째 재시도: 최초 실패 후 약 1분 뒤에 수행됩니다.
  • 두 번째 재시도: 첫 번째 재시도 후 약 4분 뒤에 수행됩니다.
두 번째 재시도 후에도 실패하면 해당 전송은 버려집니다. 각 전송 시도(최초 시도와 재시도)는 대상의 전송 탭에 별도 항목으로 Business Console에 기록되므로, 어떤 시도든 다시 보내거나 확인할 수 있습니다. 대상의 전송 로그에 있는 각 시도에는 attempt 번호가 포함됩니다. 이벤트의 첫 번째 전송은 1이며, 재시도나 다시 보내기는 반복하는 시도 번호에 1을 더한 값을 가집니다. 자동 재시도는 대상의 url을 그 후에 변경했더라도 최초 시도가 전송된 URL로 전송됩니다. 수동 다시 보내기는 대상의 현재 URL로 전송되므로, URL을 수정하면 다시 보내기도 함께 해결됩니다. 알아 두어야 할 기타 전송 규칙은 다음과 같습니다.
  • 아웃바운드 HTTP 요청은 5초 후에 시간 초과됩니다.
  • Didit은 SSRF 방어를 위해 비공개 또는 localhost URL로의 전송을 차단하며, 리디렉션을 따르지 않습니다. 직접 응답하는 공개 HTTPS endpoint를 사용하십시오.
  • 원시 백슬래시, 제어 문자, 또는 URL의 권한 부분(authority)에 @가 둘 이상 포함된 대상 url은 잘못된 형식으로 거부됩니다. 이러한 URL은 생성할 수 없으며, 이미 존재하는 URL도 다시 전송하거나 재생할 수 없습니다. 사용자 정보 구간이 최대 하나만 포함된 일반 https://host/path URL을 사용하십시오(기본 인증 방식의 webhook URL에 해당합니다).
  • 2xx는 성공으로 처리됩니다. 시간 초과와 연결 실패는 재시도 대상이며(전송 로그에는 504/503으로 표시됩니다), 404를 제외한 3xx와 4xx 응답은 재시도되지 않습니다.
  • 재시도할 때 Didit은 새 X-Timestamp로 서명을 다시 계산하므로, 타임스탬프와 서명 쌍은 항상 일치합니다.
  • 재시도와 다시 보내기는 전송되는 시점의 대상 현재 서명 비밀로 서명됩니다.

전송 순서

Didit은 webhook이 도착하는 순서를 보장하지 않습니다. 각 전송은 개별적으로 대기열에 들어가며 재시도는 최초 시도 몇 분 뒤에 도착하므로, In Progress에 대한 더 이전 이벤트보다 Approved에 대한 status.updated가 먼저 도착할 수 있습니다. 이 점을 고려하여 수신기를 구현하십시오.
  • webhook은 무언가 변경되었다는 신호로 취급하십시오. 순서가 중요한 경우에는 GET /v3/session/{session_id}/decision/을 호출하고 현재 status를 기준으로 작업하십시오.
  • 이전 이벤트가 최신 상태를 덮어쓰지 않도록 하십시오. 이벤트의 created_at을 해당 레코드에 대해 마지막으로 저장한 값과 비교하고, 더 오래된 이벤트는 무시하십시오.
  • 중복은 event_id를 기준으로 판단하십시오. 이 값은 재시도 간에도 동일하게 유지됩니다.
Didit은 Travel Rule 전송의 이벤트를 Travel Rule 교환 상태 변경에 설명된 고정된 순서로 대기열에 넣습니다. 그러나 재시도로 인해 이벤트가 도착하는 순서는 바뀔 수 있습니다.

대상 설정하기

Business Console > API & Webhooks에서 또는 Management API를 통해 대상을 관리할 수 있습니다.
1

대상 생성

POST /v3/webhook/destinations/를 호출하거나(콘솔에서 대상 추가를 클릭해도 됩니다), label, 공개 url, webhook_version("v3"), 그리고 이 endpoint가 수신해야 하는 모든 이벤트 유형을 담은 subscribed_events 배열을 지정하십시오. 최소 하나의 이벤트가 필요하며 와일드카드는 지원되지 않습니다.
2

secret_shared_key 저장

생성 응답에는 대상의 secret_shared_key가 반환됩니다. 이 값을 다른 서버 비밀 값과 함께 저장하십시오. 이 값은 이 대상에만 속하며, 세 가지 HMAC-SHA256 서명 방식 모두의 입력값입니다. GET /v3/webhook/destinations/{destination_uuid}/는 API 키 호출자와 webhook을 읽을 수 있는 콘솔 사용자에게 이 값을 다시 반환합니다.
3

endpoint 구현

공개 HTTPS POST 경로를 노출하고, 원시 본문을 읽고, 서명을 검증하고(X-Signature-V2를 우선으로 사용합니다), 타임스탬프 범위를 확인한 다음 webhook_type에 따라 분기 처리하십시오. 작업을 대기열에 넣자마자 2xx를 반환하고, 무거운 처리는 비동기로 수행하십시오.
4

수신 확인

대상의 전송 탭을 확인하십시오. 정상적인 webhook은 1~2초 안에 200을 반환합니다. 5xx/404는 Didit의 재시도 정책을 실행하며, 지속적으로 실패하면 두 번째 재시도 후에 버려집니다.
5

Try Webhook으로 반복 테스트

Try Webhook 콘솔 시나리오를 사용하여 승인됨, 거부됨, 검토 중, KYB, 엔터티, 활동, 거래 페이로드로 endpoint를 테스트한 다음 운영 트래픽을 켜십시오.
Business Console의 Didit webhook 테스트 기능

운영 환경으로 전환하기 전에 Business Console에서 직접 테스트 webhook을 전송하여 endpoint를 검증하십시오.

필요한 만큼 대상을 얼마든지 만들 수 있습니다. 일반적인 방식은 소비자별로 대상 하나를 두는 것입니다. 예를 들어 KYC 이벤트는 인증 서비스로, KYB 이벤트는 컴플라이언스 운영 서비스로, 거래 이벤트는 사기 탐지 대기열로 보낼 수 있습니다. 각 대상은 고유한 비밀, 버전, 구독 목록을 가지므로, 한 소비자의 비밀을 교체하거나 잘못 배포하더라도 다른 소비자에게는 영향을 주지 않습니다.

코드 샘플

웹훅 endpoint의 보안을 확보하려면 대상의 secret_shared_key를 사용해 수신 요청의 진위를 검증하십시오.
X-Signature-V2를 사용하십시오. 미들웨어가 본문을 다시 인코딩하는 상황에서 가장 안정적으로 동작하며, decision을 포함한 본문 전체를 완전하게 인증합니다.

Node.js / Express (권장: X-Signature-V2)

파서가 처리하기 전의 원시 바이트를 읽을 수 있다면, 원래의 X-Signature 방식도 같은 방식으로 동작하지만 원시 본문에 서명합니다. 완전성을 위해 아래 레거시 섹션에 남겨 두었습니다.

Python / FastAPI (권장: X-Signature-V2)

PHP / Laravel (권장: X-Signature-V2)

PHP의 JSON 기본값은 정규 형식을 재현하지 않습니다. 네 가지 기본값을 재정의해야 하며, 하나라도 빠지면 서명 대상 바이트가 달라져 다이제스트가 일치하지 않습니다. 가장 큰 문제를 일으키는 것은 빈 객체 문제입니다. Didit payload에는 risk_view.countries.risk_scores, risk_view.crimes.risk_scores, risk_view.custom_list와 같은 빈 맵이 자주 포함됩니다. 따라서 연관 배열로 변환했다가 되돌리면 드문 예외 상황이 아니라 일반적인 승인 세션에서도 서명이 깨집니다.
추가로 알아 둘 사항은 다음과 같습니다.
  • 정수로 표현되는 부동소수점 값은 PHP에서 별도로 처리할 필요가 없습니다. Didit은 이를 정수로 직렬화합니다(100.0 → 100). PHP의 json_encode도 정수 값을 가진 부동소수점을 같은 방식으로 기록하므로, 위의 Node.js 샘플과 달리 shortenFloats 단계가 필요하지 않습니다.
  • serialize_precision은 -1로 유지하십시오(PHP 7.1부터의 기본값). 호스트가 이 값을 17로 재정의하면 json_encode가 87.42를 87.420000000000002로 기록하여 모든 X-Signature-V2 검사가 실패합니다.
정규 형식을 맞출 수 없다면 위의 verifySignatureRaw가 더 간단한 대체 방법입니다. Laravel의 $request->getContent()는 변경되지 않은 바이트를 반환하므로, PHP 앞단에서 본문을 다시 인코딩하지 않는 한 X-Signature가 동작합니다. 이는 X-Signature-V2가 견디도록 설계된 상황이며, 그래서 V2를 먼저 시도합니다.

테스트

엔드포인트를 점검하기 위해 전체 검증을 실행 할 필요는 없습니다.
  • Business Console > API & Webhooks > Try Webhook에서 시나리오를 선택합니다(예: approved_full_features, declined_face_mismatch, in_review_aml_hit, business_session_approved, transaction_created, activity_created, user_status_updated). 그런 다음 완전한 형태의 웹훅을 대상 URL로 전송합니다.
  • 모든 시나리오는 프로덕션 트래픽과 동일한 세 가지 서명 헤더와 정규 인코딩으로 서명되며, 유니코드 테스트 데이터(approved_kyc_with_unicode)도 포함됩니다. 따라서 미들웨어를 거쳐도 X-Signature-V2가 동작하는지 확인할 수 있습니다. 테스트 전송에는 샘플 데이터가 포함되며, 프로덕션 상태에 반영되지 않도록 추가 헤더 X-Didit-Test-Webhook: true가 붙고 User-Agent에 (Test) 접미사가 붙습니다.
  • 회귀 문제를 디버깅해야 한다면, 대상의 전송 탭에서 과거 전송 건을 재생할 수 있습니다.
엔드투엔드 스모크 테스트를 하려면 자체 애플리케이션의 테스트 워크플로를 통해 실제 인증을 실행하십시오. 이때 발생하는 웹훅은 Try Webhook 시나리오와 동일한 envelope, 헤더, 재시도 정책을 사용합니다.

예시

승인된 KYC 세션 (status.updated)

경고가 포함된 거부된 세션 (status.updated)

승인된 기업 세션 (status.updated, KYB)

거래 생성됨 (transaction.created)

Travel Rule 교환 상태 변경됨 (travel_rule.status.updated)

travel_rule_status에는 Travel Rule에 문서화된 13개 교환 상태 중 하나가 담깁니다. rail은 INTERNAL, TRP, GTR, TRUST, VERIFYVASP, SYGNA, EMAIL 중 하나입니다. 레일이 선택되기 전이거나 레일을 확인할 수 없는 경우, 그리고 sunrise 등록에서는 항상 빈 문자열입니다. Didit Test VASP가 응답한 샌드박스 전송에는 TEST_VASP가 담깁니다. direction은 INBOUND 또는 OUTBOUND입니다. 샌드박스 이벤트에는 "environment": "sandbox"가 포함됩니다. TEST_VASP 이벤트에는 test_vasp: true와 test_vasp_scenario(응답한 시나리오)도 포함됩니다. Didit이 Travel Rule 전송을 위한 거래를 생성하면(POST /v3/transactions/ 호출, 입금 후 sunrise 등록, 인바운드 TRP 문의, 또는 INTERNAL 레일의 인바운드 구간), 해당 거래에 대한 transaction.created 이벤트를 먼저 보내고, 그 뒤에 전송의 travel_rule.status.updated 이벤트를 보냅니다. 해당 단계에서 규칙이 거래 상태를 바꾸면 transaction.created 앞에 transaction.status.updated가 올 수도 있습니다. 이미 생성한 입금 거래에 sunrise 등록이 연결되는 경우에는 transaction.created가 전송되지 않습니다. 각 webhook은 개별적으로 전송되고 재시도되므로, 재시도된 이벤트가 이후 이벤트보다 늦게 도착할 수 있습니다. transaction_id를 기준으로 핸들러를 구현하고, 현재 상태가 필요할 때는 거래를 다시 조회하십시오. TRUST로 보낸 전송은 COMPLETED 이벤트를 받지 않습니다. TRUST는 Didit이 읽을 수 있는 응답을 보내지 않으므로, 전송은 확인 기한까지 AWAITING_COUNTERPARTY 상태를 유지하다가 EXPIRED로 끝납니다. GTR, VerifyVASP, Sygna Bridge에서는 Didit이 네트워크에서 상대방의 응답을 읽고, 이벤트는 그 응답 뒤에 전송됩니다(Travel Rule 네트워크 참조).

워크플로 컴플라이언스 변경됨 (workflow.compliance_changed)

transition.from과 transition.to는 compliant, gaps_detected, pending_change, unknown_coverage, not_evaluated 중 하나입니다. severity는 공백이 발견된 경우 actionable(조치 필요), 대기 중인 변경에는 indicative(참고용), 공백이 해소된 경우 informative(정보 제공용)입니다. 변경된 워크플로마다 이벤트 하나가 전송됩니다.

거래 규칙 제안 생성됨 (transaction_rule_suggestions.created)

kind는 새 규칙이면 ADD, 기존 규칙의 변경이면 TWEAK, 규칙을 끄는 경우 DISABLE입니다. ADD의 경우 rule_id는 null입니다. open_count는 실행 후 아직 열려 있는 제안의 수이며, 새로 생성된 제안도 포함됩니다. 규칙 제안 설정에서 이 webhook을 켜거나 끌 수 있습니다. 거래 webhook을 참조하십시오.

Investigator 이벤트 (investigator.*)

investigator.* 이벤트 12개는 다른 모든 이벤트와 같은 봉투(event_id, webhook_type, timestamp, created_at, application_id, environment)와 같은 서명 헤더를 사용합니다. 이 이벤트에는 ID와 수치만 담깁니다. 이름, 검토자가 입력한 사유, 서술문, 최종 사용자가 작성한 내용은 절대 포함되지 않습니다. session_id는 없으며, 대상 항목은 item에 명시됩니다.
provenance는 보고서를 작성한 Investigator의 버전을 나타냅니다.
  • engine은 Investigator 엔진의 버전으로, 불투명한 8자 ID입니다. 엔진이 바뀔 때마다 변경되므로, engine 값이 같은 두 보고서는 같은 엔진이 작성한 것입니다.
  • agent_version은 에이전트의 버전입니다.
  • policy_version은 보고서가 따른 귀사 정책의 버전입니다.
  • model_id는 더 이상 사용되지 않는 별칭이며, engine과 같은 값을 담습니다. engine을 대신 읽으십시오.
  • prompt_hash는 더 이상 전송되지 않습니다.
item.kind는 항목의 종류(세션, 기업 세션, 거래 또는 케이스)를, item.id는 해당 항목의 ID를 나타냅니다. _at으로 끝나는 타임스탬프는 ISO 8601 문자열입니다. 다른 이벤트와 같은 방식으로, subscribed_events에 정확한 문자열을 나열하여 이 이벤트를 구독합니다.

사용자 엔터티 상태 변경됨 (user.status.updated)

재제출된 세션

레거시: 원시 본문의 X-Signature

X-Signature는 모든 전송에 여전히 포함됩니다. Didit이 전송하는 정확한 바이트에 대한 HMAC-SHA256 값이며(json.dumps(..., sort_keys=True, separators=(",", ":"))), 미들웨어가 본문을 수정하기 전에 원시 본문을 읽을 수 있는 경우에만 사용하십시오.

레거시: V2 webhook 형식

아래 필드는 대상이 webhook_version: "v2"로 고정된 경우에만 적용됩니다. V2 이전에 생성된 대상을 위한 더 오래된 "v1" 고정 설정도 있습니다(단수형 결정 구조라는 세 번째 형식). 이에 의존하기 전에 지원팀에 문의하십시오. 새 연동에는 위에서 설명한 V3(기본값)을 사용하십시오. V3는 복수형 배열을 사용합니다.
기능별 객체 자체는 두 버전에서 동일합니다. V2 대상은 V3와 똑같이 히트별 match_score, risk_score, score_breakdown, risk_view, review_status와 집계 score를 받습니다(AML 보고서 참조). V2로 고정하면 AML 보고서가 decision에서 놓이는 위치만 바뀌며, 포함되는 내용은 바뀌지 않습니다. 하위 소비자에게 여전히 V2 형식이 필요하다면 webhook_version: "v2"로 고정한 별도의 대상을 만들고, 새 소비자는 V3를 사용하십시오.

관련 문서

  • webhook 대상 API — 대상 목록 조회, 생성, 수정, 삭제 및 전송 로그 확인.
  • webhook 대상 생성 — 대상의 secret_shared_key를 포함한 요청 및 응답 스키마.
  • 데이터 모델 — decision에 포함된 모든 기능별 객체의 표준 스키마.
  • 결정 스키마 — 세션 webhook에 포함된 decision 객체의 전체 필드 목록.
  • 인증 상태 — 위에서 참조한 정확한 status 값과 상태 전이.
  • 속도 제한 — GET /v3/session/{id}/decision/를 대체 수단으로 폴링할 때 적용되는 제한.
마지막 수정일 2026년 11월 7일