웹훅은 식별 이벤트를 실시간으로 서버에 전달합니다. 방문자가 식별될 때마다
TRACIO는 구성된 웹훅 URL로 HTTP POST 요청을 보냅니다. 요청 본문이 곧 이벤트
페이로드입니다 — 별도로 감싸는 엔벨로프는 없습니다.
본문은 평탄한 camelCase JSON 문서입니다. 내부 이벤트에서 선별한 공개 하위
집합으로, 핑거프린트 해시나 내부 탐지 마커는 포함하지 않습니다.
{ "requestId": "1710432000_abc123def", "phase": "primary", "visitorId": "X7fh2Hg9LkMn3pQr", "linkedId": "user_12345", "tag": "login", "timestamp": "2024-03-12T16:00:00Z", "url": "https://your-app.com/login", "ip": "94.142.239.124", "userAgent": "Mozilla/5.0 …", "browser": { "name": "Chrome", "version": "120.0" }, "os": { "name": "macOS", "version": "14.3" }, "device": "desktop", "geo": { "country": "CZ", "city": "Prague", "lat": 50.05, "lon": 14.4, "timezone": "Europe/Prague", "isp": "Example ISP" }, "network": { "vpn": false, "proxy": false, "tor": false, "datacenter": false, "connectionType": "residential" }, "bot": { "result": "human", "type": "", "score": 0.02 }, "identification": { "confidence": 0.95, "incognito": false, "visitType": "returning" }, "decision": { "action": "allow", "riskScore": 4, "suspectScore": 0.08 }}| Field | Type | Description |
|---|---|---|
requestId | string | 고유 이벤트 식별자(멱등성 키로도 사용) |
phase | string | primary 또는 late — 아래 참고 |
visitorId | string | 안정적인 방문자 식별자 |
linkedId | string | 클라이언트가 제공한 연결 식별자 |
tag | string | 클라이언트가 제공한 사용자 지정 태그 |
timestamp | string | 이벤트 시각(RFC 3339) |
url | string | 이벤트가 캡처된 페이지 URL |
ip | string | 클라이언트 IP 주소 |
userAgent | string | 원시 클라이언트 user-agent 문자열 |
browser.name / .version | string | 탐지된 브라우저 |
os.name / .version | string | 탐지된 운영체제 |
device | string | 기기 분류(예: desktop, mobile) |
geo | object | IP 지리위치: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter(불리언)와 connectionType |
bot.result | string | human, bot 또는 uncertain |
bot.type | string | 봇이 탐지된 경우의 자유 형식 자동화 라벨 |
bot.score | number | 봇 확률 점수 |
identification.confidence | number | 모델 신뢰도(0.0–1.0) |
identification.incognito | boolean | 시크릿/비공개 브라우징 컨텍스트 |
identification.visitType | string | 방문 분류 |
decision.action | string | 권장 조치 |
decision.riskScore | number | 종합 위험 점수(0–100) |
decision.suspectScore | number | 세분화된 의심 점수 |
phase는 동일한 requestId를 공유하는 두 번의 전달을 구분합니다:
primary — 방문자가 식별될 때 즉시 전송됩니다.late — 후속 보강 이벤트(약간 늦게 캡처된, 정교화된 봇 판정과
행동 신호).두 이벤트는 requestId로 상관 짓고 phase로 구별하세요.
모든 전달에는 X-Tracio-Signature 헤더가 포함됩니다:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t는 요청이 서명된 Unix 타임스탬프(초)입니다.v1은 웹훅 시크릿을 키로 하여 "<t>.<rawRequestBody>"를 계산한
HMAC-SHA256의 16진수 인코딩 값입니다.타임스탬프는 서명 대상 콘텐츠의 일부이므로 재전송 공격 방지를 제공합니다 —
검증은 반드시 원시 요청 본문 Buffer에 대해 수행하세요(파싱/재직렬화된
JSON을 사용하면 서명이 일치하지 않습니다). 서명 대상 콘텐츠는 바이트로
구성하세요: "<t>." 접두사에 원시 본문 버퍼를 이어 붙인 뒤 이를 HMAC 처리합니다.
// Express.js exampleimport express from "express"import crypto from "crypto"
const app = express()
// Capture the raw body so we can verify the signature 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 { // Parse "t=...,v1=..." const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string])) const t = parts["t"] const v1 = parts["v1"] if (!t || !v1) return false
// Sign the raw bytes: "<t>." prefix + the raw request body buffer. const signed = Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]) const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex")
const a = Buffer.from(v1, "hex") const b = Buffer.from(expected, "hex") return a.length === b.length && crypto.timingSafeEqual(a, b)}
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.visitorId}`) console.log(`Bot: ${event.bot.result}`) // "human" | "bot" | "uncertain"
res.status(200).send("OK")})추가 재전송 방어책으로, t가 현재 시각과 너무 크게 벌어진(예를 들어 5분 이상
차이 나는) 전달을 거부하는 것도 고려할 수 있습니다.
| Header | Description |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | "<t>.<rawBody>"에 대한 t=<unix>,v1=<hmac_sha256_hex> |
X-Tracio-Event-Id | 이벤트의 requestId — 멱등성 키로 사용하세요 |
X-Tracio-Webhook-Id | 이 전달을 생성한 웹훅의 식별자 |
웹훅을 관리하는 가장 간단한 방법은 대시보드에서 시각적으로 하는 것입니다.
워크스페이스 범위의 관리 API를 통해 프로그램적으로도 관리할 수 있으며, 이는
대시보드가 사용하는 것과 동일한 API입니다. 이 API는 애플리케이션 호스트(예:
https://app.tracio.ai/api/v1)에서 제공되며 대시보드 세션(Clerk) JWT로
인증됩니다. 모든 요청은 추가로 워크스페이스 역할(RBAC)에 대해 검사됩니다.
독립적인 API 시크릿은 없습니다. 모든 웹훅 엔드포인트는
/workspaces/{workspaceId} 아래에 있습니다.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{workspaceId}/webhooks \ -H "Authorization: Bearer <clerk-session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'서명 시크릿은 TRACIO가 생성하며 생성 시(및 회전 시) signingSecret 아래에
한 번만 반환됩니다. 안전하게 보관하세요 — 서명을 검증하는 데 사용하는 키입니다.
{ "ok": true, "data": { "id": "wh_abc123", "workspaceEnvironmentId": "b201f2ba-…", "url": "https://your-server.com/webhook/tracio", "events": [], "signingSecret": "f3a9…<hex>", "status": "active", "successRate": 100, "createdAt": "2024-03-12T16:00:00Z" }}이후 읽기에서는 signingSecret이 마스킹됩니다(null) — 생성과 시크릿 회전에서만
공개됩니다.
| Method | Path | Description |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | 웹훅 목록 조회 |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | url / events / status 업데이트 |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | 웹훅 삭제 |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | 서명된 테스트 전달 발송 |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | 서명 시크릿 회전 |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | 최근 전달 시도 목록 조회 |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | 최근 전달 시도 목록 조회 |
전달은 재시도될 수 있으므로 핸들러는 멱등적이어야 합니다. X-Tracio-Event-Id
헤더(requestId)로 중복을 제거하세요:
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")})타임아웃을 피하려면 가능한 한 빨리 2xx 응답을 반환하고 페이로드는 비동기로
처리하세요. 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.riskScore > 50) { await alertFraudTeam(event) }
if (event.bot.result === "bot") { await blockVisitor(event.visitorId) }}웹훅의 Test 작업(또는 POST .../webhooks/:webhookId/test)을 사용하여
서명된 샘플 페이로드를 엔드포인트로 보내고, 접근 가능한지 그리고 서명을 올바르게
검증하는지 확인하세요.
로컬 개발 시에는 ngrok과 같은 터널로 서버를 노출하세요:
ngrok http 3000# Use the generated URL as your webhook endpoint