웹훅은 식별 이벤트를 실시간으로 서버에 전달합니다. 방문자가 식별될 때마다 TRACIO는
구성된 웹훅 URL로 HTTP POST 요청을 보냅니다. 요청 본문이 곧 이벤트
페이로드입니다.
또한 지연 판정 — 페이지가 이미 로드된 뒤 방문자의 행동을 통해 자동화임이 드러난 경우 — 을 전달하는 유일한 채널이기도 합니다.
대시보드의 설정 → Webhooks에서 설정합니다. 웹훅은 Pro 요금제 이상에서 사용할 수 있습니다.
| 이벤트 | 시점 | 요금제 |
|---|---|---|
identification | 모든 방문에서 — primary, late, correction 단계 | 전체 |
account_takeover | 계정에서의 행동이 더 이상 소유자 프로필과 일치하지 않을 때 | Business 이상 |
account_assessment | 계정의 계정 탈취 평가 상태: 판정되었고 이상 없음, 판정되었고 어긋남, 또는 판정되지 않음 | Business 이상 |
attack_detected | 사이트에서 봇이 급증할 때 | Business 이상 |
reputation_changed | 연결된 사람의 평판이 다른 구간으로 옮겨 갔을 때 | Business 이상 |
transaction_state_changed | POST /v2/transactions로 보낸 거래의 결과가 바뀌었을 때 | Business 이상 |
login_risk | POST /v2/logins/assess의 로그인 평가가 challenge 또는 review로 끝났을 때 | 전체 |
trusted_device_enrolled | 기기가 계정의 신뢰 기기로 등록되었거나 그 신뢰가 갱신되었을 때 | 전체 |
trusted_device_revoked | 기기의 신뢰가 철회되었을 때 — API를 통해서든 대시보드에서든 | 전체 |
affiliate_threshold_crossed | 제휴사에 대한 권고가 나빠졌을 때(none → review, none → hold, review → hold) | 전체 |
이벤트 이름은 밑줄을 쓰며 점은 결코 쓰지 않습니다 — visitor.created나
session.created 같은 것은 없습니다.
웹훅은 특정 유형을 구독합니다. 별도의 값 *는 "나중에 추가되는 것을 포함한 모든
유형"을 뜻합니다. 다섯 가지 유형은 예외입니다: *에는 account_assessment,
login_risk, trusted_device_enrolled, trusted_device_revoked,
affiliate_threshold_crossed가 포함되지 않으므로 — 각각을 이름으로 명시하세요.
account_assessment는 탐지 결과가 아니라 평가 범위를 보고하므로, linked_id를 담은
방문마다 대략 한 번씩 도착합니다 — 다른 이벤트와는 규모가 한 자릿수 이상 다릅니다.
로그인 위험과 제휴사 유형은 해당 API를 호출하는 연동에서만 의미가 있습니다. 이
이벤트들이 생기기 전에 설정한 와일드카드는 이를 수신하겠다는 동의가 아닙니다.
알 수 없는 유형은 구독을 만들거나 수정할 때 400으로 거부되므로, 오타 때문에
한 번도 발화하지 않는 웹훅이 조용히 남는 일은 없습니다.
identification 이벤트의 단계한 번의 방문은 **동일한 request_id**를 공유하는 최대 3건의 전달을 만들어냅니다:
primary — 페이지 로드 시점의 최초 판정.late — 약 9초 뒤, 느린 검사들이 도착한 시점의 보강.correction — 행동(포인터, 키보드, 스크롤)에 기반한 정정.request_id로 상관 짓고 phase로 구분하세요. 나중 단계가 우선합니다: primary가
human이라고 했고 correction이 bot이라고 한다면, 정답은 두 번째입니다.
도착 순서에 의존하지 마세요. 각 단계는 독립적으로, 각자의 재시도 일정에 따라
전달됩니다 — primary가 재시도에 들어간 사이 late가 첫 시도에 성공하면 순서가
뒤바뀌어 도착합니다. 우선순위는 수신 시각이 아니라 phase 필드로 판단하세요.
identification 이벤트의 단계는 이 세 가지뿐입니다. 다만 다른 값 하나가 더
도착합니다: account_takeover와 account_assessment는 phase: "beacon"을 담고
옵니다. 둘 다 세션의 행동 비컨에서 올라오기 때문입니다.
여기서 생기는 불일치에 유의하세요. 멱등성에 영향을 주기 때문입니다. 프로덕션의
identification 전달은 event_id가 정확히 <request_id>:<phase>이지만, 다른
전달들은 이 공식을 벗어납니다. account_takeover는 <request_id>:ato이며 — 접미사는
리터럴 ato이지 phase 필드의 값이 아닙니다 — account_assessment는
<request_id>:ato-state-<state>입니다. 대시보드에서 보낸 테스트 전달은
<request_id>:test인데, 그 본문의 phase는 여전히 primary로 나옵니다 — 게다가
스키마 1 본문에는 phase 필드 자체가 없으므로, 그 접미사가 보이는 곳은 헤더뿐입니다.
event_id를 그대로 멱등성 키로 쓰고, request_id와 phase로 다시 조립하는 일은
결코 하지 마세요. 직접 처리하는 값에 대해서만 매칭하고, 나머지는 전달을 거부하는
대신 무시하세요.
방문이 없는 이벤트도 있습니다: attack_detected(워크스페이스 수준),
reputation_changed(사람 수준), transaction_state_changed(거래 수준),
login_risk, trusted_device_enrolled, trusted_device_revoked(계정 수준),
affiliate_threshold_crossed(제휴사 수준). 이들에는 request_id도 visitor_id도
없고 browser, geo, bot, decision 블록도 없습니다 — 해당 키들은
X-Tracio-Request-Id 헤더와 마찬가지로 아예 존재하지 않습니다. 반면
account_takeover와 account_assessment는 특정 방문에서 생성되며, 요금제에 해당하는
완전한 식별 본문을 담습니다. 모든 이벤트를 하나의 핸들러에서 파싱한다면, 방문 관련
필드를 건드리기 전에 event를 확인하세요.
새 웹훅은 페이로드 버전 3을 사용합니다. 버전 1 또는 2를 쓰는 기존 웹훅은 선택한 버전을 유지합니다. 두 이전 스키마는 동결되었으며 2027-04-12에 제거됩니다. 마이그레이션하려면 대시보드의 웹훅 카드에서 버전 3을 선택하세요.
| 버전 | 상태 |
|---|---|
1 | 동결됨. 2027-04-12에 제거 |
2 | 동결됨. 2027-04-12에 제거 |
3 | 새 웹훅의 기본값. snake_case 키 |
버전 3은 버전 2와 같은 필드와 값을 담되, 키는 snake_case이고 최상위에
version: 3이 있습니다. 서명 알고리즘과 헤더는 바뀌지 않습니다
(X-Tracio-Payload-Version: 3). 직접 정의한 fields 사전 안의 키는 여러분의
데이터이므로 이름이 바뀌지 않습니다.
| v2 | v3 |
|---|---|
eventId | event_id |
requestId | request_id |
visitorId | visitor_id |
linkedId | linked_id |
userAgent | user_agent |
network.connectionType | network.connection_type |
network.proxyDetected | network.proxy_detected |
network.realIp | network.real_ip |
bot.antidetectScore | bot.antidetect_score |
identification.matchType | identification.match_type |
identification.matchConfidence | identification.match_confidence |
identification.incognitoVisits | identification.incognito_visits |
identification.driftScore | identification.drift_score |
decision.riskScore | decision.risk_score |
decision.suspectScore | decision.suspect_score |
deviceInfo | device_info |
deviceInfo.deviceId | device_info.device_id |
deviceInfo.crossBrowser | device_info.cross_browser |
deviceInfo.linkedBrowsers | device_info.linked_browsers |
screen.colorDepth | screen.color_depth |
screen.pixelRatio | screen.pixel_ratio |
clientHints | client_hints |
clientHints.platformVersion | client_hints.platform_version |
clientHints.deviceName | client_hints.device_name |
osEnvironment | os_environment |
environment.virtualMachine | environment.virtual_machine |
spoofing.spoofedAxes | spoofing.spoofed_axes |
spoofing.anonymousBrowser | spoofing.anonymous_browser |
session.durationSeconds | session.duration_seconds |
velocity.uniqueIps | velocity.unique_ips |
velocity.uniqueLocations | velocity.unique_locations |
accountAlert | account_alert |
accountAlert.accountId | account_alert.account_id |
accountAlert.driftScore | account_alert.drift_score |
accountAssessment | account_assessment |
accountAssessment.accountId | account_assessment.account_id |
accountAssessment.profile.windowsEffective | account_assessment.profile.windows_effective |
accountAssessment.profile.oldestWindowAgeSeconds | account_assessment.profile.oldest_window_age_seconds |
attack.windowMinutes | attack.window_minutes |
attack.recentBots | attack.recent_bots |
attack.recentTotal | attack.recent_total |
personId | person_id |
reputation.overallRisk | reputation.overall_risk |
linkedAccountsCount | linked_accounts_count |
linkedVisitorsCount | linked_visitors_count |
reputationChange | reputation_change |
reputationChange.personId | reputation_change.person_id |
reputationChange.previousBand | reputation_change.previous_band |
reputationChange.visitorIds | reputation_change.visitor_ids |
reputationChange.linkedAccountsCount | reputation_change.linked_accounts_count |
{ "version": 3, "event": "identification", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:primary", // "<request_id>:<phase>" — the idempotency key "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", // visit identifier, shared by all phases "phase": "primary", "visitor_id": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "linked_id": "user-42", // your ?lid=, if you passed one "tag": "checkout", "fields": { "order_ref": "A-1029", "plan": "pro" }, // custom fields that arrived with the visit "timestamp": "2026-07-30T12:00:00Z", "url": "https://shop.example.com/checkout", "ip": "203.0.113.44", "user_agent": "Mozilla/5.0 …", "browser": { "name": "Chrome", "version": "138" }, "os": { "name": "macOS", "version": "15.5" }, "device": "desktop", "gpu": "Intel Iris Plus Graphics 655", // 비디오 어댑터 모델, 정규화됨. 알 수 없으면 존재하지 않음 "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.405, "timezone": "Europe/Berlin" }, "network": { "vpn": false, "proxy": true, "tor": false, "datacenter": true, "connection_type": "DCH", "proxy_detected": true // HTTP 트래픽과 원시 네트워크 경로가 서로 다른 네트워크로 나감 }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "risk_score": 12.2 }, // real | fake | suspicious "extensions": ["metamask", "privacy_badger"] // detected browser extensions; absent when none}0과 빈 값은 생략됩니다. 값이 0인 문자열·숫자 필드(예: 사람에 대한 bot.type이나
bot.antidetect_score: 0)는 JSON에 존재하지 않습니다 — 스키마에서 필수로 두지 말고
중첩 블록은 방어적으로 읽으세요.
bot.score와 decision.risk_score는 소수점 첫째 자리까지 표기되는 0..100
스케일의 소수입니다 — 같은 방문에 대해 대시보드와 Data API가 보고하는 바로 그
숫자입니다. (동결된 v1 스키마에서는 단위가 다릅니다: 각각 0..1 비율과 0..255입니다.)
bot.type은 인식된 봇의 이름, 검증된 AI 에이전트(ai_agent:<vendor>) 또는 계열
레이블입니다. 어휘는 봇 유형을 참고하세요 — 내부 검사
이름은 어떤 요금제에서도 노출되지 않습니다. ai_agent:<vendor>는 요청이 user agent에서
그렇다고 주장하기만 한 것이 아니라, 해당 벤더의 인프라에서 왔음을 증명했다는 뜻입니다 —
벤더가 게시한 키로 만든 유효한 Web Bot Auth 서명이거나, 벤더가 게시한 IP 대역 안의
주소입니다. AI 에이전트라고 주장하지만 벤더가 게시한 증거와 하나같이 어긋나는 요청은
impersonation으로 보고됩니다.
| 필드 | 타입 | 설명 |
|---|---|---|
version | number | 페이로드 스키마 버전(3) |
event | string | 이벤트 유형 |
event_id | string | 전달 식별자 — 멱등성 키. 이벤트별로 고유하며, 그 이벤트를 받는 여러분의 모든 웹훅이 공유하고, 재시도 간에도 유지됩니다 |
request_id | string | 방문 식별자(UUID). 그 방문의 모든 단계가 공유합니다 |
phase | string | primary, late, correction. account_takeover와 account_assessment는 beacon을 담음 |
visitor_id | string | 안정적인 방문자 식별자 |
linked_id | string | 클라이언트가 전달한 연결 식별자 |
tag | string | 클라이언트가 전달한 커스텀 태그 |
fields | object | 수집 시점에 방문에 첨부된 커스텀 필드. 방문과 함께 도착한 것만 담습니다 — 나중에 Data API로 첨부한 필드는 어떤 단계나 재시도에서도 웹훅에 나타나지 않습니다. 키와 값은 문자열입니다. 방문에 필드가 없었다면 존재하지 않습니다 |
timestamp | string | 이벤트 시각(RFC 3339) |
url | string | 이벤트가 수집된 페이지 URL |
ip | string | 클라이언트 IP 주소 |
user_agent | string | 원본 클라이언트 user-agent 문자열 |
browser.name / .version | string | 탐지된 브라우저 |
os.name / .version | string | 탐지된 운영체제 |
device | string | 디바이스 분류(예: desktop, mobile) |
gpu | string | 브라우저가 보고하는 비디오 어댑터 모델(WebGL)로, 읽기 쉬운 이름으로 정규화됩니다(Intel Iris Xe Graphics, Apple M1 Pro). 소프트웨어 래스터라이저는 Software renderer. 알 수 없으면 존재하지 않습니다 |
geo | object | IP 위치 정보: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter(불리언)와 connection_type |
network.proxy_detected | boolean | 방문의 HTTP 트래픽과 원시 네트워크 경로가 서로 다른 네트워크로 나갑니다 — 브라우저 앞에 프록시나 VPN이 있다는 뜻입니다. 같은 사업자의 주소 두 개(통신사 NAT, 같은 VPN의 두 번째 출구)는 해당하지 않습니다 |
bot.result | string | human, bot 또는 uncertain |
bot.type | string | 봇이 탐지되었을 때의 봇 이름, 검증된 AI 에이전트 또는 계열 레이블 |
bot.score | number | 봇 점수(0–100) |
identification.confidence | number | 식별 신뢰도(0.0–1.0) |
identification.incognito | boolean | 비공개/시크릿 브라우징 컨텍스트 |
decision.action | string | real, fake 또는 suspicious |
decision.risk_score | number | 종합 위험 점수(0–100) |
extensions | array | 이 방문에서 탐지된 브라우저 확장 프로그램으로, 안정적인 기계용 슬러그로 표기됩니다(이름, 카테고리, 스토어 링크는 대시보드와 Data API에 있습니다). 탐지된 것이 없으면 존재하지 않습니다 |
Pro 이상 — 방문자가 시간에 걸쳐 어떻게 행동하는지:
{ "identification": { "match_type": "exact", // exact | fuzzy | new — how the visitor was recognized "match_confidence": 0.93, "visits": 42, "incognito_visits": 3 }, // Present when visitor counters are available at event time (usually primary). // A missing block means "no data", not "zeros". "velocity": { "events5m": 7, "unique_ips": 2, "unique_locations": 1 }, "bot": { "antidetect_score": 0 }, // antidetect indicators, 0..100 "session": { "duration_seconds": 95 } // the correction phase and the account_takeover alert}Business 이상 — 판정이 그렇게 나온 이유:
{ "reasons": [ // at most 8, sorted by importance { "code": "headless_browser", "severity": "high" }, { "code": "privacy_hardening", "severity": "low" } ], // Behavioral biometrics — present only when behavioral scoring ran for the // visit. A missing block means "no data", never "nothing suspicious". "behavior": { "score": 87, "verdict": "human", "confidence": 0.92 }, // The smoothed behavior score the account-takeover verdict was made on. It is // measured by beacons that arrive after the identification event, so it is // empty in identification deliveries — see the account_takeover event. "identification": { "drift_score": 0.41 }, "device_info": { "device_id": "…", // the physical device across browsers on it "cross_browser": true, "confidence": 0.88, "linked_browsers": 3 }, "network": { "isp": "Deutsche Telekom", "asn": 3320, // 원시 네트워크 경로에서 관측된 공인 주소, 즉 프록시나 VPN 뒤에 있는 주소. // 그런 주소가 관측되지 않으면 존재하지 않음 "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // 리눅스 머신에서 실제로 측정된 데스크톱 환경("Mint 22+", "Ubuntu", // "GNOME", "KDE"). User-Agent는 배포판을 표현할 수 없음. 판별되지 않으면 // 존재하지 않으며, 대부분의 리눅스 방문과 리눅스가 아닌 모든 방문이 여기에 // 해당함. "os_environment": "Mint 22+", // 방문이 스스로 주장한 바와 독립적인 검사가 측정한 바의 대비. 위조가 // 실제로 탐지되었을 때만 존재함. `real` 필드가 비어 있다는 것은 "검사가 // 침묵했다"는 뜻이지 "확인됨"이 아님. `gpu` 축에서는 `claimed.gpu`가 // 주장된 어댑터를 최상위 `gpu` 필드와 동일한 읽기 쉬운 모델 이름으로 담음. "spoofing": { "detected": true, "claimed": { "os": "Windows 10", "browser": "Chrome 139.0" }, "real": { "os": "macOS" }, "spoofed_axes": ["os", "screen"], // os | gpu | screen | network | browser "anonymous_browser": { "detected": true, "names": ["Linken Sphere"] } }, // 기기 사실 — 방문자의 브라우저가 머신에 대해 보고하는 내용으로, 당사 // 측에서 정제한 것. screen: 해상도, 색 심도, device pixel ratio. locale: // 브라우저 자체가 선호하는 언어와 시간대로, IP 주소에서 도출되는 // geo.timezone과는 다르며, 둘 사이의 불일치는 위치 위조의 흔한 신호임. // client_hints: User-Agent Client Hints — CPU 아키텍처와 비트 수, 기기 // 모델(Android에서는 `model`의 모델 코드, 예: "SM-A556B", 그리고 Google // Play 기기 목록에 실린 마케팅 이름이 `device_name`에, 예: // "Samsung Galaxy A55 5G"), 정확한 플랫폼 버전. Chromium 기반 브라우저만 // 보고함. // 방문이 그런 데이터를 담지 않았다면 각 블록은 존재하지 않으므로 모두 // 선택적인 것으로 다룰 것. "screen": { "width": 2560, "height": 1600, "color_depth": 30, "pixel_ratio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "client_hints": { "architecture": "arm", "bitness": "64", "platform_version": "15.5.0" }, // 비디오 어댑터가 스스로를 가상이라고 밝힌 경우에만 존재함. hypervisor는 // 닫힌 사전임(vmware, virtualbox, parallels, qemu, hyperv, bochs, // intel-gvt, vgpu). 블록이 없다는 것은 그런 증거가 없다는 뜻임. "environment": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}이유 코드 어휘와 severity의 의미는
봇 탐지를 참고하세요.
다중 계정 필드(Business 이상). 워크스페이스에서 person 모드가 활성화되면 본문에는
person_id, reputation 블록(score, categories, overall_risk),
linked_accounts_count, linked_visitors_count도 담깁니다 — 방문자가 다른 방문자 및
계정과 연결되어 있는지를 나타냅니다. person 레이어는 현재 관찰 모드로 동작하며, 정식
가동되기 전까지 이 필드들은 본문에 존재하지 않습니다. 스키마에서 필수로 두지 마세요.
guidance는 통합 지점별로 "무엇을 할지"에 대한 완성된 권고를 담고 있어, 원시 점수에서
정책을 직접 도출할 필요가 없습니다. 모든 v3 Business+ 이벤트에 존재하며 — allow도
명시적으로 표기됩니다:
{ "guidance": { "version": 3, "overall": "review", // the strictest advice across the scenarios "payment": "review", // whether to accept the payment "registration": "challenge", // whether to create the account "login": "challenge", // whether to let them into the account "affiliate": "review", // whether to credit the conversion to the partner "basis": ["risk", "network"] // the axes that determined the advice }}모든 시나리오는 allow에서 시작해 사다리를 위로만 올라갑니다:
allow → challenge → review → deny. 시나리오 안에서는 발화한 축 중 가장 엄격한
것이 이기고, overall은 네 시나리오 전체에서 가장 엄격한 값입니다.
| 권고 | 결제 | 가입 | 로그인 | 제휴 |
|---|---|---|---|---|
allow | 처리하세요 | 생성하세요 | 들여보내세요 | 전환을 인정하세요 |
challenge | 3-D Secure / 확인 | 캡차, 이메일 또는 전화 확인 | 단계적 2FA, 재인증 | 활동이 확인될 때까지 의심으로 표시 |
review | 처리하되 검토 대기열에 넣으세요 | 제한을 두고 생성하세요 | 들여보내되 알림을 올리세요 | 검토가 끝날 때까지 지급을 보류하세요 |
deny | 거래를 처리하지 마세요 | 계정 생성을 거부하세요 | 들여보내지 마세요 | 전환을 인정하지 마세요 |
basis는 권고를 결정한 축을 나열합니다 — 채택된 것보다 약한 제안을 낸 축은 목록에
들어가지 않습니다. 고정된 집합을 가정하지 말고 코드로 읽으세요:
축(basis) | 의미 |
|---|---|
list | 방문자가 enforce 모드인 워크스페이스 목록 중 하나와 일치했습니다. allow 목록은 모든 축을 무시합니다: 모든 시나리오가 allow가 되고 basis는 정확히 ["list"]입니다 |
rule | 방문이 enforce 모드인 워크스페이스 규칙 중 하나와 일치했습니다. 규칙은 자기 시나리오의 권고만 움직이며(allow 규칙은 그 시나리오만 완화합니다), 목록이 이미 작동했다면 건너뜁니다 |
bot | 봇 탐지기가 자동화라고 결론 내렸습니다 — 모든 시나리오를 거부합니다 |
risk | 방문의 판정과 종합 위험 점수 |
behavior | 행동 생체 인식이 충분한 신뢰도로 봇이라고 말합니다 — 거부가 아니라 마찰을 더합니다 |
account_takeover | 이 계정에서의 행동이 더 이상 소유자의 프로필과 일치하지 않습니다 — 로그인은 challenge, 결제는 review로 |
network | Tor, 또는 데이터센터에서 나오는 VPN |
affiliate_pattern | 자체 생성된 추천의 형태 — 제휴 전환은 challenge로 보류됩니다 |
fraud_reputation, multiaccounting | 방문자 뒤에 있는 사람의 평판과 계정 군집. person 레이어가 정식 가동되면 나타납니다 |
version은 규칙 집합의 버전으로, 로직이 개선될 때 올라가므로 로그에 남겨 두세요.
버전 2는 권고를 완화할 수 있는 첫 축인 list 축을 도입했고, 버전 3은
account_takeover의 로그인 임계값을 계정 탈취 판정 점수의 스케일로 옮겼습니다. 그래서
같은 방문이라도 버전 2에서 challenge였던 것이 deny가 될 수 있고, 그 반대도
가능합니다.
가이던스는 가산적입니다: 새로운 시나리오는 새 키로 도착하며 계약을 깨지 않습니다. 나중
단계가 이기지만 부분 권고는 예외입니다: 불완전한 입력으로 계산된 전달에는
"partial": true가 표시되며, 부분 권고는 같은 request_id에 대해 앞서 받은 완전한
권고를 덮어쓰지 않습니다. 일반적인 전달에서는 partial 필드가 아예 없습니다.
정확한 임계값은 의도적으로 문서화하지 않습니다. 점수로 역산할 수 있는 권고는 더 이상
방어 수단이 아닙니다. 의도된 예외는 정확히 하나입니다: 아래에서 설명하는 계정 탈취
판정의 결정 경계인 account_assessment.verdict.threshold입니다.
account_takeover 이벤트Business와 Enterprise 전용입니다. 하위 요금제에서는 웹훅이 이 이벤트를 구독하고 있어도
전달이 만들어지지 않습니다. 본문은 요금제에 해당하는 완전한 식별 엔벨로프에
account_alert 블록과 다음 절에서 설명하는 account_assessment 블록을 더한 것이며,
한 방문당 최대 한 번 전달됩니다:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // 경보는 비컨에서 올라온다. "ato"라고 말하는 것은 event_id뿐 "account_alert": { "type": "behavior-drift/2", // kind of alert and the version of its construction "account_id": "user-42", // your linked_id for the account "drift_score": 0.71 // the value the verdict was made on }, "account_assessment": { "type": "behavior-drift/2", "account_id": "user-42", "state": "assessed_alert", "verdict": { "severity": "high", "score": 0.71, "threshold": 0.3565, "scale": "manhattan-normalized", "basis": "ring", "windows": 9 }, "profile": { "windows": 6, "windows_effective": 5, "oldest_window_age_seconds": 1837 } } // …the remaining identification fields}type에 버전이 붙는 이유. 점수의 구성은 이전에도 바뀐 적이 있습니다: 판정의
스케일과 기준이 그렇습니다. behavior-drift/2는 버전이 붙은 첫 구성입니다. 이 숫자에
맞춰 자체 규칙을 보정했다면, 이 값이 바뀌는 것을 규칙을 다시 점검하라는 신호로
받아들이세요.
account_alert.drift_score와 identification.drift_score는 같은 숫자입니다
(2026-10-03부터): 판정의 근거가 된 평활화된 행동 점수로, 계정 소유자의 과거 신뢰 창
링에 비추어 측정됩니다. 그 날짜 이전의 identification.drift_score는 다른 스케일의
원시 거리를 담았으므로 — 날짜를 넘나들며 값을 섞지 마세요. 창이 판정되지 않았다면 이
필드는 0이 아니라 존재하지 않습니다. 숫자를 비교 대상이 된 임계값과 함께 봐야 한다면
account_assessment.verdict를 사용하세요.
v1에서 이 블록은 type, linkedId, drift를 담은 accountAlert이며, 평가 블록은
없습니다. v2에서는 경보의 두 필드 이름이 바뀌고 — linkedId → accountId,
drift → driftScore — accountAssessment가 추가됩니다. v3는 위의 snake_case
이름을 씁니다. 버전을 전환할 때 핸들러를 함께 업데이트하지 않으면, 계정 탈취 로직이
조용히 데이터를 보지 못하게 됩니다.
account_assessment 이벤트Business와 Enterprise 전용입니다. 명시적인 구독이 필요합니다: *에는 포함되지
않습니다.
이 이벤트가 없으면 경보의 부재는 서로 다른 두 가지를 동시에 뜻합니다: 세션을 계정 소유자의 프로필에 비추어 평가했고 일치했거나, 아예 평가할 수 없었거나 — 소유자의 프로필이 준비되지 않았거나, 창이 너무 희박했거나, 트래픽이 사람이 아니었던 경우입니다. 둘 다 침묵처럼 보이고, 침묵은 안전으로 읽힙니다. 대개는 그렇지 않습니다: 2026년 9월의 어느 프로덕션 하루 동안 우리는 창의 73%와 계정의 80%에 대해 판정을 보류했습니다. "경보가 없으면 괜찮다"에 기반한 프로세스는 대부분의 경우 우리에게 없는 상태 위에 세워져 있는 셈입니다.
{ "version": 3, "event": "account_assessment", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato-state-not_assessed", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", "account_assessment": { "type": "behavior-drift/2", "account_id": "user-42", "state": "not_assessed", // always present, one of three values "reason": "profile_not_ready", // only when state is not_assessed "profile": { "windows": 3, // windows in the owner's reference ring "windows_effective": 2, // of those, how many yielded a distance "oldest_window_age_seconds": 1837 } } // …the remaining identification fields}state는 결코 빠지지 않으며 결코 비어 있지 않습니다:
state | 의미 | 할 일 |
|---|---|---|
assessed_clear | 판정됨. 행동이 소유자와 일치함 | 없음 |
assessed_alert | 판정됨. 행동이 어긋남 | 단계적 인증 — account_takeover 경보가 같은 판정을 담습니다 |
not_assessed | 판정이 내려지지 않음 | 깨끗한 것으로 취급하지 마세요. 자체 신호로 대체하세요 |
reason(not_assessed일 때만):
reason | 의미 |
|---|---|
profile_not_ready | 소유자의 기준이 현재 이 계정에 대해 판정을 낼 수 없습니다. 단계가 아니라 상태로 다루세요: 영영 바뀌지 않을 수도 있으므로 기다리지 마세요 |
profile_learning | 기준이 아직 축적되는 중입니다 |
window_too_sparse | 사람은 있었지만 창에 담긴 상호작용이 너무 적었습니다 |
not_computable | 창과 기준에 공통된 정보성 축이 없었습니다 |
bot_traffic | 데이터는 충분했지만 판정할 사람이 없었습니다 |
2026-10-04 18:00 UTC부터, 기준이 준비된 소유자에 대해서는 window_too_sparse와
not_computable이 더 이상 발행되지 않습니다 — 그런 창은 profile_not_ready로
보고됩니다. 이전 전달이 이 두 사유를 담고 있으므로 목록에는 남겨 둡니다: 핸들러가
이들에 따라 분기한다면 profile_not_ready도 같은 방식으로 처리하고, 그 날짜를
넘나들며 이 사유들의 비율을 비교하지 마세요.
state와 reason은 요금제로 제한되지 않습니다. verdict는 drift_score와 마찬가지로
Business+입니다.
verdict 블록은 assessed_clear와 assessed_alert에만 존재합니다.
not_assessed일 때는 블록이 0으로 채워지는 대신 존재하지 않습니다 — threshold 0
옆의 score 0은 "행동이 정확히 일치했다"로 읽힐 것이기 때문입니다.
| 필드 | 의미 |
|---|---|
severity | low 또는 high — 의도적으로 두 구간입니다: 중간 구간을 두려면 같은 스케일에 두 번째 임계값이 필요합니다 |
score | 판정의 근거가 된 값 |
threshold | 실제로 비교 대상이 된 값 |
scale | 점수가 만들어지는 방식: manhattan-normalized는 정규화된 행동 공간에서의 d/(1+d)이며, 0은 정확한 일치를 뜻합니다 |
basis | ring — 소유자의 과거 신뢰 창 중 가장 가까운 것까지의 거리. centroid — 평균 프로필까지의 거리 |
windows | 점수에 집계된 창의 수 |
score와 threshold는 언제나 이름이 붙은 같은 스케일 위에 있으므로, 이 쌍으로 결정을
재현할 수 있습니다: 경보는 정확히 score > threshold일 때 발화합니다.
얼마나 자주 도착하는가. 방문당 상태별로 최대 한 건의 전달 — 실제로는 행동 창마다가
아니라 linked_id를 담은 방문당 한 건입니다. 한 방문 안에서 상태가 바뀔 수 있으며 —
예를 들어 기준의 학습이 끝나 not_assessed가 assessed_clear가 되는 경우 — 그때는 두
건을 순서대로 받습니다. 이 전환은 가능할 뿐 약속된 것은 아닙니다: profile_not_ready는
계정의 수명 내내 지속될 수 있습니다.
약속하지 않는 것. 가장 잘 보호되는 계정은 세션이 길고 기준 프로필이 완성된 활성 사용자이고, 가장 덜 보호되는 계정은 드물게 쓰는 사용자로, 이들에게는 탈취가 더 위험합니다. 이것은 구성의 속성이지 조정할 수 있는 임계값이 아닙니다. 이 이벤트는 기준에서 벗어난 행동을 보고할 뿐 — 계정 탈취 일반을 잡아낸다고 주장하지 않습니다.
attack_detected 이벤트봇 급증 경보는 attack 블록이 담긴 공통 v3 엔벨로프로 도착합니다. 식별 필드는 없습니다
— 워크스페이스 수준 이벤트이기 때문입니다:
{ "version": 3, "event": "attack_detected", "event_id": "c0a8e1f2-…", // alert id; matches X-Tracio-Event-Id "timestamp": "2026-07-30T12:00:00Z", "attack": { "kind": "bot_spike", "severity": "critical", // warning | critical "window_minutes": 15, "recent_bots": 4210, "recent_total": 5100, "expected": 180.5 // the baseline expected over a window this size }}페이로드 버전 1의 웹훅은 기존의 평면 경보 스키마를 받으며 — 이것은 바뀌지 않습니다.
login_risk, trusted_device_enrolled, trusted_device_revoked 이벤트이 세 이벤트는 로그인 위험 API(POST /v2/logins/assess)와 그 신뢰 기기에 대한 여러분
자신의 호출에서 비롯됩니다. 이름으로 명시한 구독에만 전달되며 — *에는 포함되지
않습니다 — 페이로드 버전 2 또는 3의 웹훅에만 전달됩니다. 버전 1에는 이들을 위한 본문이
없습니다.
login_risk는 평가가 challenge 또는 review를 반환할 때 전송되며 — allow,
deny, not_assessed에서는 결코 전송되지 않습니다. 그 login_risk 블록은 저장된
평가로, 동기 응답이 준 것과 정확히 같습니다. 이벤트 id: login-risk:<id>이며,
<id>는 저장된 레코드에 대한 당사의 식별자입니다 — 여러분의 assessment_id는
본문에서 가져오세요.trusted_device_enrolled와 trusted_device_revoked는 신뢰 기기의 등록, 갱신,
철회가 있을 때마다 전송됩니다. 그 trusted_device 블록 — user_id, device_ref,
enrolled_at, expires_at, revoked_at, revoked_by(api 또는 dashboard) —
은 이벤트 시점의 스냅숏입니다. trusted_device_enrolled에서는 revoked_at과
revoked_by가 null입니다. 이벤트 id: trusted-device:<n>.이들은 호출 후 최대 약 5초 안에 비동기로 전송되며, X-Tracio-Request-Id 헤더를 담지
않습니다 — 방문을 찾으려면 login_risk.request_id를 사용하세요. 웹훅은 감사와 알림을
위한 통지이며, 결정 자체는 동기 응답입니다.
{ "version": 3, "event": "login_risk", "event_id": "login-risk:4b1f9e2c-6d3a-4f80-9b7e-2a5c8d1e0f37", "timestamp": "2026-10-17T10:00:00Z", "login_risk": { "assessment_id": "snap-1", "user_id": "alice", "state": "assessed", "recommendation": "challenge", "reasons": ["new_device"] // …the rest of the assessment }}{ "version": 3, "event": "trusted_device_revoked", "event_id": "trusted-device:7", "timestamp": "2026-10-17T11:00:00Z", "trusted_device": { "user_id": "alice", "device_ref": "7978151c54e94838", "enrolled_at": "2026-10-17T10:00:00Z", "expires_at": "2026-11-16T10:00:00Z", "revoked_at": "2026-10-17T11:00:00Z", "revoked_by": "dashboard" }}affiliate_threshold_crossed 이벤트이름으로 명시한 구독에만 전달되며 — *에는 포함되지 않습니다 — 페이로드 버전
2 또는 3의 웹훅에만 전달됩니다. TRACIO는 15분마다 데이터가 충분한 각 제휴사의 품질
점수를 다시 계산하고, 권고가 나빠지면 이벤트를 보냅니다: none → review,
none → hold 또는 review → hold. 개선되면 아무것도 보내지 않으며, 다음 악화가 다시
보고될 수 있게 됩니다. 이 이벤트는 워크스페이스에 속합니다: 모든 환경의 구독 웹훅으로
가며 X-Tracio-Request-Id 헤더를 담지 않습니다. 이벤트 id:
affiliate-threshold:<affiliate_id>:<recommendation>:<unix time>.
{ "version": 3, "event": "affiliate_threshold_crossed", "event_id": "affiliate-threshold:aff_17:review:1792490400", "timestamp": "2026-10-20T10:00:00Z", "affiliate_threshold": { "affiliate_id": "aff_17", "recommendation": "review", // none | review | hold "previous_recommendation": "none", "quality_score": 72, // 0..100, higher is better "hold_days": 14, // the current hold period of the workspace "crossed_at": "2026-10-20T10:00:00Z", "metrics": { "reg_to_ftd": 0.31, "active14_share": 0.42, "dup_device_share": 0.3, "dup_person_share": 0.1, "ring_share": 0.05, "bonus_hunter_share": 0.2, "dc_vpn_bot_share": 0.1, "chargeback_share": 0.03, "affiliate_fraud_share": 0, "no_visit_share": 0.05 } }}metrics는 최근 90일 동안의 제휴사 비율로, 대시보드 보고서가 보여주는 것과 같습니다:
첫 입금에 도달한 가입의 비율(reg_to_ftd), 가입 14일 후에도 여전히 활동 중인 플레이어의
비율(active14_share), 그리고 중복 기기 또는 중복 사람이 있는 전환, 사기 링에서 온 전환,
보너스 헌터에게서 온 전환, 데이터센터·VPN·봇에서 온 전환, 차지백 또는 제휴 사기로
레이블된 전환, 방문이 발견되지 않은 전환의 비율입니다. TRACIO는 권고만 합니다 — 보류
자체와 지급 결정은 여러분의 제휴 플랫폼에 남습니다. 페이로드 버전 2에서는 같은 본문이
camelCase 키를 가집니다: 블록은 affiliateThreshold이고, affiliateId,
previousRecommendation, qualityScore 등이 들어 있습니다.
transaction_state_changed 이벤트Business 이상. POST /v2/transactions로 보낸 거래의 결과가 바뀔 때 전송됩니다: 규칙이
REVIEW 또는 DECLINE을 결정했을 때(APPROVE는 아무것도 보내지 않습니다), 분석가가
대시보드에서 거래를 검토로 보냈을 때, 또는 레이블이 설정·교체·제거되었을 때입니다.
청구가 검토 중인 동안 보너스를 보류하고, 결과가 확정되면 폴링 없이 보너스를 보류하거나
지급하는 데 사용하세요. *에는 이 이벤트가 포함됩니다. 페이로드 버전 2 또는 3의
웹훅에만 전달됩니다. 환경에 묶인 웹훅은 그 환경에 대해서만 이 이벤트를 받습니다.
{ "version": 3, "event": "transaction_state_changed", "event_id": "txn-state:7d1c2b3a-0000-4000-8000-000000000042:118", "timestamp": "2026-10-15T10:04:00Z", "transaction": { "id": "7d1c2b3a-0000-4000-8000-000000000042", "transaction_id": "bonus-claim-1842", "state": "REVIEW", // always the decision of the rules "fraud_score": 50, "effective_state": "DECLINE", // the outcome to act on "recommendations": ["withhold_bonus"], "label": "fraud_detection_bonus_abuse", "label_source": "api", // api | dashboard "label_at": "2026-10-15T10:04:00Z", "previous_effective_state": "REVIEW" // …applied_rules and evidence, as in the transaction response }}| 키 | 의미 |
|---|---|
effective_state | 행동의 근거로 삼을 결과: 분석가나 레이블의 상태가 설정되어 있으면 그것, 아니면 state |
recommendations | 보너스를 어떻게 할지. effective_state에서 도출됩니다 |
label, label_source, label_at | 현재 레이블, 설정된 곳(api 또는 dashboard)과 시점. 거래에 레이블이 없으면 세 값이 모두 null입니다 |
previous_effective_state | 이번 변경 전의 effective_state. 자동 결정 시 보내는 이벤트에서는 null입니다 |
이 블록은 거래를 이벤트가 전송되는 시점의 모습으로 보여줍니다: 두 변경이 빠르게
이어지면 두 이벤트 모두 최신 상태를 담되, 각자의 previous_effective_state와
timestamp를 가집니다. 순서는 보장되지 않습니다 — timestamp로 정렬하고, 현재
상태는 API로 거래를 읽어 확인하세요. 이벤트 id: 자동 결정은 txn-state:<id>:created,
이후의 변경은 매번 txn-state:<id>:<n>입니다. 이 이벤트는 누가 레이블을 설정했는지는
알려주지 않고 — 어디서 왔는지만 알려줍니다.
reputation_changed 이벤트Business 이상. 여러분의 조직이 본 적 있는 사람의 고객 간 평판이 다른 구간으로 옮겨 갈 때
(예: clean → suspicious) 전송됩니다. person 레이어가 필요하며, 이 레이어는 현재 관찰
모드로 동작하므로 — 기기 간 신원 결합이 활성화된 워크스페이스에서만 발생합니다. 이것은
사람 수준 이벤트입니다: request_id도 방문 블록도 없습니다.
{ "version": 3, "event": "reputation_changed", "event_id": "person-reputation:pn_4f2a…:clean->suspicious:2026-07-30T12:10:00.123456Z", "timestamp": "2026-07-30T12:10:00Z", "reputation_change": { "person_id": "pn_4f2a…", "score": 46, "band": "suspicious", // clean | suspicious | fake "previous_band": "clean", "categories": ["chargeback"], "visitor_ids": ["X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"], // the person's visitors in YOUR data "linked_accounts_count": 4 // omitted when zero }}블록 키는 reputation이 아니라 reputation_change입니다: 전자는 특정 방문 맥락에서의
평판이 이미 쓰고 있으며, 둘은 서로 다른 것입니다. 스키마 1은 기존의 평면 형태 — 같은
필드를 최상위에 — 로 전달합니다.
모든 전달에는 X-Tracio-Signature 헤더가 포함됩니다:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t는 요청이 서명된 Unix 타임스탬프(초)입니다.v1은 웹훅 시크릿을 키로 사용한 "<t>.<rawRequestBody>"의 16진 인코딩된
HMAC-SHA256입니다.타임스탬프는 서명 대상 콘텐츠의 일부이며, 이것이 재전송 공격 방지를 제공합니다.
제대로 해야 할 두 가지가 있습니다. 그렇지 않으면 프로덕션에서 검증이 실패합니다:
v1= 값 중 하나라도 일치하면 받아들이세요. 시크릿 교체 기간 동안 헤더에는
두 개의 서명이 담기며, 그중 하나만 남기는 파서는 교체 기간 내내 정상적인 전달을
거부하게 됩니다.// Express.js exampleimport express from "express"import crypto from "crypto"
const app = express()
// Capture the raw body so the signature can be verified byte-for-byte.app.use( express.json({ verify: (req, _res, buf) => { ;(req as any).rawBody = buf }, }),)
function verifySignature(rawBody: Buffer, header: string, secret: string): boolean { if (!header) return false
const parts = header.split(",").map((p) => p.trim()) const ts = parts.find((p) => p.startsWith("t="))?.slice(2) if (!ts) return false
// Replay protection: reject timestamps more than five minutes old. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
// Sign the raw bytes: the "<t>." prefix plus the raw request body. const signed = Buffer.concat([Buffer.from(`${ts}.`, "utf8"), rawBody]) const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex") const exp = Buffer.from(expected, "hex")
// During a rotation window the header carries several v1= — any may match. return parts.some((p) => { if (!p.startsWith("v1=")) return false const got = Buffer.from(p.slice(3), "hex") // Compare lengths BEFORE timingSafeEqual: it throws on differing lengths, // and one junk header would turn the handler into a 500. return got.length === exp.length && crypto.timingSafeEqual(got, exp) })}
app.post("/webhook/tracio", (req, res) => { const header = req.headers["x-tracio-signature"] as string if (!verifySignature((req as any).rawBody, header, WEBHOOK_SECRET)) { return res.status(401).json({ error: "Invalid signature" }) }
const event = req.body console.log(`Visitor: ${event.visitor_id}`) console.log(`Bot: ${event.bot?.result}`) // "human" | "bot" | "uncertain"
res.status(200).send("OK")})스키마 2와 3 전달에는 추가로 X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>)가 포함됩니다. HMAC 시크릿은 양쪽이 다 알고 있으므로
HMAC은 발신자가 시크릿을 안다는 것은 증명하지만 요청이 TRACIO에서 비롯되었다는 것은
증명하지 못합니다. 비대칭 서명이 그것을 증명합니다. 공개 키는
https://api.tracio.ai/.well-known/webhook-keys에 kid를 키로 하여 게시됩니다:
public_key 필드를 읽으세요. 레거시 별칭 publicKey는 같은 값을 담으며
2027-04-12에 제거됩니다.
대시보드에서 보내는 테스트 전달은 HMAC으로만 서명됩니다 — 플랫폼 개인 키는 전달
노드에 있으며 의도적으로 대시보드에서는 사용할 수 없습니다. Ed25519를 필수로
요구하는 검증기는 테스트 전달을 통과시켜야 합니다(테스트 전달은 event_id에 :test
접미사가 붙습니다). 그렇지 않으면 프로덕션은 정상인데 대시보드에서의 테스트만
실패합니다. 같은 주의가 형식 검사에도 적용됩니다: 테스트 전달은 request_id가
test_<hex> 형태이고 visitor_id는 리터럴 test_visitor이므로, 이것들을 프로덕션
형식에 비추어 검증하는 핸들러는 그 밖에는 문제가 없는 전달을 거부하게 됩니다.
교체 후 두 시크릿이 24시간 동안 유효하게 유지되고 헤더에 두 서명이 모두 담기므로, 전달을 잃지 않고 설정을 갱신할 수 있습니다. 지금 폐기 작업은 이 기간을 앞당겨 끝냅니다. 24시간 안에 사용하는 쪽의 시크릿을 갱신하세요: 기간이 닫히면 이전 시크릿은 더 이상 일치하지 않으며, 잘못된 서명에 대해 엔드포인트가 4xx로 응답한다면 그런 응답이 연속 5회 발생할 때 웹훅이 비활성화됩니다.
| 헤더 | 설명 |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — 교체 기간에는 v1=이 두 개 |
X-Tracio-Signature-Ed25519 | 플랫폼 서명, t=<unix>,kid=<id>,v1=<base64>(v2와 v3 전용) |
X-Tracio-Event-Id | 전달 식별자 — 멱등성 키 |
X-Tracio-Request-Id | 방문 식별자, request_id와 일치(v2와 v3 전용). 방문 이벤트 — identification, account_takeover, account_assessment — 에만 전송됩니다 |
X-Tracio-Event-Type | 이벤트 유형(v2와 v3 전용) |
X-Tracio-Delivery-Attempt | 시도 번호, 1부터 시작(v2와 v3 전용) |
X-Tracio-Payload-Version | 3 또는 2(v2와 v3 전용) |
X-Tracio-Webhook-Id | 이 전달을 생성한 웹훅의 식별자 |
전달은 재시도될 수 있으며, 재시도는 동일한 X-Tracio-Event-Id를 담습니다. 이를 기준으로
중복을 제거하세요:
app.post("/webhook/tracio", async (req, res) => { const eventId = req.headers["x-tracio-event-id"] as string
const existing = await db.webhooks.findOne({ eventId }) if (existing) return res.status(200).send("Already processed")
await db.webhooks.insert({ eventId, processedAt: new Date() }) await processWebhookEvent(req.body)
res.status(200).send("OK")})event_id는 웹훅별이 아니라 이벤트별로 고유하다는 점에 유의하세요: 워크스페이스의
여러 웹훅이 같은 이벤트를 구독하고 있다면, 각각 동일한 식별자를 가진 전달을 받습니다.
identification의 경우 이 값은 <request_id>:<phase>로 만들어지므로, 한 방문의 세
단계는 하나로 합쳐지지 않고 각각 독립적으로 중복 제거됩니다. 방문이 없는 이벤트는 각자의
형태를 가집니다 — 위의 각 이벤트를 참고하세요.
2xx로 응답하세요 — 전달이 수락되었음을 알리는 유일한 신호입니다.
| 응답 | 동작 |
|---|---|
2xx | 전달 완료 |
429 Too Many Requests | 실패로 세지 않고 시도를 소모하지도 않습니다. 더 긴 Retry-After는 존중됩니다 |
408, 425, 5xx, 연결 끊김 | 대기 시간을 늘려 가며 재시도합니다 |
410 Gone | 엔드포인트가 제거된 것으로 간주되어 웹훅이 즉시 비활성화됩니다 |
그 밖의 4xx | 재시도하지만 연속 5회면 웹훅이 비활성화됩니다 — 400/401/404는 재시도로 낫지 않습니다 |
재시도 일정: 5s → 30s → 2min → 10min → 30min → 2h → 6h(8회 시도). 초기 재시도는 1분 안에 들어가므로 서비스를 잠깐 재시작해도 알림을 잃지 않습니다. 각 대기 시간은 절반에서 전체 값 사이로 무작위화되어, 장애 복구 후 재시도가 한꺼번에 몰리지 않습니다.
자동 비활성화에는 임계값(연속 20회 실패 또는 5회 구성 오류)과 최소 15분간 연속된 실패가 모두 필요합니다 — 많은 전달이 큐에 쌓였더라도 잠깐의 재시작으로 연동이 끊기지는 않습니다. 15분보다 긴 공백이 생기면 카운트가 처음부터 다시 시작됩니다. 대시보드에는 응답 코드와 오류 텍스트가 담긴 사유가 표시되며, 카운터를 초기화하는 다시 활성화 버튼이 있습니다.
| 요금제 | 워크스페이스당 웹훅 수 |
|---|---|
| Free | 제공되지 않음 |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
엔드포인트는 공인 IP를 가진 https여야 하며 — 사설 주소와 루프백 주소는 리다이렉트
경로에서도 거부됩니다 — 리다이렉트는 2단계까지만 허용됩니다.
따라가는 리다이렉트는 307과 308뿐입니다. 301, 302, 303은 클라이언트에
GET으로 전환하고 본문을 버리라고 지시하므로 전달은 이를 따라가지 않으며, 그 시도는
실패로 계산됩니다. 로드 밸런서가 URL을 정규화한다면(www나 끝 슬래시 추가), 웹훅을
최종 URL로 바로 지정하세요.
웹훅은 대시보드에서 관리합니다. 대시보드는 워크스페이스 범위의 관리 API를
구동하며, 이 API는 애플리케이션 호스트(예: https://app.tracio.ai/api/v1)에서
제공됩니다. 아래 엔드포인트가 바로 대시보드가 호출하는 것들입니다. 모든 웹훅
엔드포인트는 /workspaces/{wsId} 아래에 있습니다.
이것은 서버 대 서버 연동면이 아닙니다. 관리 API는 워크스페이스 역할(RBAC)에 비추어 검사되는 대시보드 세션 JWT만 받아들이며,
tracio_sk_…시크릿 키는 여기서 거부됩니다. 그 세션은 브라우저 안에 있고 브라우저와 함께 만료되므로, 아래 호출들은 자동화할 연동 대상이 아니라 대시보드가 무엇을 하는지에 대한 설명으로 보시기 바랍니다. 자체 백엔드에서 프로그램으로 접근하려면 읽기 전용 Data API를 사용하세요.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{wsId}/webhooks \ -H "Authorization: Bearer <session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'서명 시크릿은 TRACIO가 생성하며 생성 시(그리고 교체 시)에 signingSecret으로 한 번만
반환됩니다. 안전하게 보관하세요 — 서명을 검증할 때 사용하는 키입니다.
{ "ok": true, "data": { "id": "b3d4f8a1-2c67-4e9b-8f05-7a1d3c9e2b48", "workspaceEnvironmentId": "b201f2ba-…", "url": "https://your-server.com/webhook/tracio", "events": [], "signingSecret": "f3a9…<hex>", "status": "active", "successRate": 100, "createdAt": "2026-07-30T12:00:00Z" }}이후 조회에서는 signingSecret이 마스킹됩니다(null) — 생성과 시크릿 교체에서만
공개됩니다.
| 메서드 | 경로 | 설명 |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | 웹훅 목록 |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | url / events / status 수정 |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | 웹훅 삭제 |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | 서명된 테스트 전달 보내기 |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | 서명 시크릿 교체 |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | 최근 전달 시도 목록 |
타임아웃을 피하려면 가능한 한 빨리 2xx를 반환하고 페이로드는 비동기로 처리하세요:
app.post("/webhook/tracio", async (req, res) => { res.status(200).send("OK") processWebhookEvent(req.body).catch(console.error)})
async function processWebhookEvent(event: WebhookPayload) { await db.events.insert(event)
if (event.decision?.risk_score > 50) { await alertFraudTeam(event) }
if (event.bot?.result === "bot") { await blockVisitor(event.visitor_id) }}웹훅의 테스트 작업(또는 POST .../webhooks/{webhookId}/test)을 사용하면 서명된
샘플 페이로드를 엔드포인트로 보내, 도달 가능한지와 서명을 올바르게 검증하는지 확인할
수 있습니다.
로컬 개발에서는 ngrok 같은 터널로 서버를 노출하세요:
ngrok http 3000# Use the generated URL as your webhook endpoint