Webhook ส่งเหตุการณ์การระบุตัวตนไปยังเซิร์ฟเวอร์ของคุณแบบเรียลไทม์ ทุกครั้งที่มี
การระบุตัวตนผู้เข้าชม TRACIO จะส่งคำขอ HTTP POST ไปยัง URL ของ webhook ที่คุณ
กำหนดค่าไว้ เนื้อหา (body) ของคำขอ คือ เพย์โหลดของเหตุการณ์โดยตรง — ไม่มี
envelope ห่อหุ้มใด ๆ
Body เป็นเอกสาร JSON แบบแบน (flat) ในรูปแบบ camelCase เป็นชุดย่อยสาธารณะที่คัดสรร
แล้วของเหตุการณ์ภายใน — ไม่มี fingerprint hash หรือมาร์กเกอร์การตรวจจับภายในใด ๆ
{ "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 | ตัวระบุเหตุการณ์ที่ไม่ซ้ำกัน (เป็น idempotency key ด้วย) |
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 (ค่า boolean) และ 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 timestamp (วินาที) ณ เวลาที่ลงลายเซ็นคำขอv1 คือ HMAC-SHA256 ที่เข้ารหัสแบบ hex ของ "<t>.<rawRequestBody>" โดยใช้
webhook secret ของคุณเป็นคีย์Timestamp เป็นส่วนหนึ่งของเนื้อหาที่ลงลายเซ็น ซึ่งให้การป้องกัน replay —
ให้ตรวจสอบเทียบกับ Buffer ของเนื้อหาคำขอ ดิบ (อย่าใช้ JSON ที่ถูกแยกวิเคราะห์/
ซีเรียลไลซ์ใหม่ มิฉะนั้นลายเซ็นจะไม่ตรงกัน) สร้างเนื้อหาที่ลงลายเซ็น
เป็นไบต์: คำนำหน้า "<t>." ต่อกับ buffer ของ body ดิบ แล้วจึงคำนวณ 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 อยู่ห่างจากเวลาปัจจุบันมากเกินไป
(เช่น เบี่ยงเบนเกินห้านาที) เพื่อเป็นการป้องกัน replay เพิ่มเติมอีกชั้นหนึ่ง
| Header | Description |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> over "<t>.<rawBody>" |
X-Tracio-Event-Id | requestId ของเหตุการณ์ — ใช้เป็น idempotency key |
X-Tracio-Webhook-Id | ตัวระบุของ webhook ที่สร้างการส่งนี้ |
วิธีที่ง่ายที่สุดในการจัดการ webhook คือทำแบบมองเห็นได้ใน dashboard นอกจากนี้ยัง
สามารถจัดการแบบโปรแกรมได้ผ่าน management API ที่มีขอบเขตต่อ workspace — เป็น API
เดียวกับที่ dashboard ใช้ โดยให้บริการบนโฮสต์ของแอปพลิเคชัน (เช่น
https://app.tracio.ai/api/v1) และตรวจสอบตัวตนด้วย JWT ของ dashboard
session (Clerk) ของคุณ ทุกคำขอจะถูกตรวจสอบเพิ่มเติมเทียบกับบทบาทใน workspace
ของคุณ (RBAC) ไม่มี API secret แบบแยกเดี่ยว endpoint ของ 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": [] }'Signing secret สร้างโดย TRACIO และส่งคืนเพียงครั้งเดียวตอนสร้าง
(และตอน rotate) ภายใต้ 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) — จะเปิดเผยเฉพาะ
ตอนสร้างและตอน rotate secret เท่านั้น
| 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 | หมุนเวียน signing secret |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | แสดงรายการความพยายามส่งล่าสุด |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | แสดงรายการความพยายามส่งล่าสุด |
การส่งอาจถูกลองใหม่ ดังนั้น handler ของคุณควรเป็น idempotent ให้กำจัดข้อมูลซ้ำโดยอิง
เฮดเดอร์ 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 ให้เร็วที่สุดเท่าที่จะทำได้ และประมวลผลเพย์โหลดแบบ
อะซิงโครนัสเพื่อหลีกเลี่ยง timeout การตอบกลับที่ไม่ใช่ 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) }}ใช้การดำเนินการ Test บน webhook (หรือ POST .../webhooks/:webhookId/test)
เพื่อส่งเพย์โหลดตัวอย่างที่ลงลายเซ็นไปยัง endpoint ของคุณ และยืนยันว่าสามารถเข้าถึงได้และ
ตรวจสอบลายเซ็นได้อย่างถูกต้อง
สำหรับการพัฒนาในเครื่อง ให้เปิดเซิร์ฟเวอร์ของคุณออกสู่ภายนอกด้วย tunnel เช่น ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint