Webhook chuyển các sự kiện nhận diện đến máy chủ của bạn theo thời gian thực. Mỗi
khi một khách truy cập được nhận diện, TRACIO gửi một yêu cầu HTTP POST đến URL
webhook mà bạn đã cấu hình. Nội dung của yêu cầu chính là payload của sự kiện —
không có lớp bao (envelope) bọc ngoài.
Body là một tài liệu JSON phẳng theo kiểu camelCase. Đây là một tập con công khai
đã được chọn lọc của sự kiện nội bộ — không chứa fingerprint hash hay các dấu hiệu
phát hiện nội bộ.
{ "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 }}| Trường | Kiểu | Mô tả |
|---|---|---|
requestId | string | Mã định danh sự kiện duy nhất (cũng là khóa idempotency) |
phase | string | primary hoặc late — xem bên dưới |
visitorId | string | Mã định danh khách truy cập ổn định |
linkedId | string | Mã định danh liên kết do client cung cấp |
tag | string | Thẻ tùy chỉnh do client cung cấp |
timestamp | string | Thời điểm sự kiện (RFC 3339) |
url | string | URL trang nơi sự kiện được ghi nhận |
ip | string | Địa chỉ IP của client |
userAgent | string | Chuỗi user-agent thô của client |
browser.name / .version | string | Trình duyệt được phát hiện |
os.name / .version | string | Hệ điều hành được phát hiện |
device | string | Loại thiết bị (ví dụ desktop, mobile) |
geo | object | Định vị theo IP: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (boolean) và connectionType |
bot.result | string | human, bot, hoặc uncertain |
bot.type | string | Nhãn tự động tự do khi phát hiện bot |
bot.score | number | Điểm xác suất bot |
identification.confidence | number | Độ tin cậy của mô hình (0.0–1.0) |
identification.incognito | boolean | Ngữ cảnh duyệt riêng tư/ẩn danh |
identification.visitType | string | Phân loại lượt truy cập |
decision.action | string | Hành động được đề xuất |
decision.riskScore | number | Điểm rủi ro tổng hợp (0–100) |
decision.suspectScore | number | Điểm nghi ngờ chi tiết |
phase phân biệt hai lần gửi cùng chia sẻ một requestId:
primary — được gửi ngay lập tức khi khách truy cập được nhận diện.late — sự kiện bổ sung làm giàu dữ liệu về sau (một phán quyết bot chính xác
hơn và các tín hiệu hành vi được ghi nhận muộn hơn một chút).Hãy tương quan hai sự kiện qua requestId và phân biệt chúng qua phase.
Mỗi lần gửi đều bao gồm một header X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t là Unix timestamp (giây) tại thời điểm yêu cầu được ký.v1 là mã HMAC-SHA256 mã hóa hex của "<t>.<rawRequestBody>", dùng khóa là
webhook secret của bạn.Timestamp là một phần của nội dung được ký, điều này mang lại khả năng chống replay —
hãy xác minh dựa trên Buffer body thô của yêu cầu (đừng dùng JSON đã được
parse/tuần tự hóa lại, nếu không chữ ký sẽ không khớp). Hãy dựng nội dung được ký
dưới dạng byte: tiền tố "<t>." nối với buffer body thô, rồi tính HMAC trên đó.
// 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")})Bạn cũng có thể muốn từ chối các lần gửi có t chênh lệch quá xa so với thời gian
hiện tại (ví dụ, lệch hơn năm phút) như một lớp bảo vệ chống replay bổ sung.
| Header | Mô tả |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> trên "<t>.<rawBody>" |
X-Tracio-Event-Id | requestId của sự kiện — dùng làm khóa idempotency |
X-Tracio-Webhook-Id | Mã định danh của webhook đã tạo ra lần gửi này |
Cách đơn giản nhất để quản lý webhook là trực quan trong dashboard. Chúng cũng có
thể được quản lý theo hướng lập trình qua management API phạm vi workspace — cùng một
API mà dashboard sử dụng. Nó được phục vụ trên host của ứng dụng (ví dụ
https://app.tracio.ai/api/v1) và được xác thực bằng JWT của phiên dashboard
(Clerk) của bạn; mỗi yêu cầu còn được kiểm tra thêm dựa trên vai trò của bạn trong
workspace (RBAC). Không có API secret độc lập. Tất cả các endpoint webhook đều nằm
dưới /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": [] }'Signing secret được TRACIO tạo ra và trả về một lần duy nhất khi tạo (và khi
rotate) dưới trường signingSecret. Hãy lưu trữ nó an toàn — đó là khóa bạn dùng để
xác minh chữ ký.
{ "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" }}Ở các lần đọc sau, signingSecret được che (null) — nó chỉ được tiết lộ khi tạo và
khi rotate secret.
| Phương thức | Đường dẫn | Mô tả |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Liệt kê webhook |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | Cập nhật url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Xóa một webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | Gửi một lần test đã ký |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | Xoay vòng signing secret |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Liệt kê các lần gửi gần đây |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Liệt kê các lần gửi gần đây |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Liệt kê các lần gửi gần đây |
Các lần gửi có thể được thử lại, vì vậy handler của bạn nên có tính idempotent. Hãy
loại bỏ trùng lặp dựa trên header X-Tracio-Event-Id (chính là 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")})Trả về phản hồi 2xx càng nhanh càng tốt và xử lý payload theo cách bất đồng bộ để
tránh timeout. Các phản hồi không phải 2xx (và lỗi kết nối) sẽ được thử lại; thất
bại lặp lại có thể tự động vô hiệu hóa 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) }}Dùng thao tác Test trên một webhook (hoặc POST .../webhooks/:webhookId/test) để
gửi một payload mẫu đã ký đến endpoint của bạn và xác nhận rằng nó có thể truy cập
được cũng như xác minh chữ ký đúng cách.
Với phát triển cục bộ, hãy phơi bày máy chủ của bạn bằng một tunnel như ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint