Webhook は識別イベントをリアルタイムでサーバーに配信します。訪問者が識別されるたびに、
TRACIO は構成された Webhook 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 }}| フィールド | 型 | 説明 |
|---|---|---|
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 を共有する 2 つの配信を区別します。
primary — 訪問者が識別された時点で直ちに送信されます。late — 後続の強化イベント(精緻化されたボット判定と、
やや後にキャプチャされた行動シグナル)です。両者は requestId で相関付けし、phase で区別します。
すべての配信には X-Tracio-Signature ヘッダーが含まれます。
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t はリクエストが署名された Unix タイムスタンプ(秒)です。v1 は、Webhook シークレットでキー付けされた "<t>.<rawRequestBody>" の
16 進エンコードされた HMAC-SHA256 です。タイムスタンプは署名対象のコンテンツの一部であり、リプレイ保護を提供します —
生のリクエストボディ 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 分を超えるずれ)を拒否することもできます。
| ヘッダー | 説明 |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | "<t>.<rawBody>" に対する t=<unix>,v1=<hmac_sha256_hex> |
X-Tracio-Event-Id | イベントの requestId — 冪等性キーとして使用 |
X-Tracio-Webhook-Id | この配信を生成した Webhook の識別子 |
Webhook を管理する最も簡単な方法は、ダッシュボードで視覚的に行うことです。
ワークスペーススコープの管理 API を通じてプログラムから管理することもできます —
ダッシュボードが使用するのと同じ API です。アプリケーションホスト(たとえば
https://app.tracio.ai/api/v1)で提供され、ダッシュボードの
セッション(Clerk) JWT で認証されます。すべてのリクエストは、加えて
ワークスペースロール(RBAC)に対してチェックされます。スタンドアロンの API シークレットはありません。すべての Webhook エンドポイントは
/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)— 作成と
シークレットローテーションによってのみ明かされます。
| メソッド | パス | 説明 |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Webhook を一覧表示 |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | url / events / status を更新 |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Webhook を削除 |
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 以外のレスポンス(および接続エラー)は
リトライされ、繰り返しの失敗は Webhook を自動的に無効化することがあります。
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) }}Webhook の Test アクション(または POST .../webhooks/:webhookId/test)を
使用して署名付きのサンプルペイロードをエンドポイントへ送信し、到達可能であり
署名を正しく検証していることを確認します。
ローカル開発では、ngrok などのトンネルでサーバーを公開します。
ngrok http 3000# Use the generated URL as your webhook endpoint