Webhook 会实时地把识别事件推送到你的服务器。每当一个访客被识别,TRACIO 就会向你配置
的 webhook URL 发送一个 HTTP POST 请求。请求体就是事件负载(payload)——不存在
任何外层包装信封。
请求体是一个扁平的 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(十六进制编码),使用你的 webhook
密钥作为密钥。时间戳是被签名内容的一部分,从而提供了重放保护——请针对原始请求体 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 与当前时间相差过大的投递(例如时间偏差超过五分钟),作为额外的
重放防护。
| 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 | 产生此次投递的 webhook 的标识符 |
管理 webhook 最简单的方式是在控制台(dashboard)中可视化操作。你也可以通过工作区范围
的管理 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)——它仅在创建和轮换密钥时才会
显示。
| Method | Path | Description |
|---|---|---|
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