Webhook gửi 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 tới URL webhook mà
bạn đã cấu hình. Thân yêu cầu chính là payload của sự kiện.
Đây cũng là kênh duy nhất gửi các phán quyết muộn — những phán quyết mà hành vi của khách truy cập chứng minh họ là tự động sau khi trang đã tải xong.
Hãy thiết lập trong dashboard tại Settings → Webhooks. Webhook yêu cầu gói Pro trở lên.
| Sự kiện | Khi nào | Gói |
|---|---|---|
identification | Ở mọi lượt truy cập — các pha primary, late và correction | Tất cả |
account_takeover | Hành vi trong một tài khoản không còn khớp với hồ sơ của chủ tài khoản | Từ Business trở lên |
attack_detected | Bot tăng vọt trên trang của bạn | Từ Business trở lên |
reputation_changed | Danh tiếng của người đứng sau một thiết bị đã thay đổi | Từ Business trở lên |
Tên sự kiện dùng dấu gạch dưới, không bao giờ dùng dấu chấm — không hề có
visitor.created hay session.created. reputation_changed cần đến lớp dữ liệu về
người, nên nó chỉ kích hoạt với những workspace đã bật phân giải danh tính xuyên thiết
bị.
Một webhook đăng ký các loại cụ thể; giá trị riêng * có nghĩa là "mọi loại, kể cả
những loại được thêm sau này". Loại không xác định sẽ bị từ chối với mã 400 khi tạo
hoặc chỉnh sửa đăng ký, nên một lỗi gõ nhầm không thể để lại cho bạn một webhook âm
thầm không bao giờ kích hoạt.
identificationMột lượt truy cập tạo ra tối đa ba lần gửi cùng chia sẻ một requestId duy nhất:
primary — phán quyết ban đầu, ngay khi trang tải.late — phần làm giàu dữ liệu khoảng chín giây sau, khi các kiểm tra chậm đã hoàn tất.correction — hiệu chỉnh dựa trên hành vi (con trỏ, bàn phím, cuộn trang).Hãy đối chiếu chúng bằng requestId và phân biệt bằng phase. Pha đến sau được ưu
tiên: nếu primary nói human còn correction nói bot thì câu trả lời đúng là
cái thứ hai.
Đừng dựa vào thứ tự đến. Mỗi pha được gửi độc lập và theo lịch thử lại riêng của
nó — nếu primary rơi vào chu trình thử lại trong khi late thành công ngay lần đầu,
bạn sẽ nhận chúng theo thứ tự ngược lại. Hãy xác định mức ưu tiên từ trường phase,
chứ không phải từ thời điểm nhận được.
Ba pha đó là toàn bộ các pha của một sự kiện identification. Vẫn còn một giá trị khác
đến được với bạn: account_takeover mang phase: "beacon", bởi cảnh báo chiếm đoạt tài
khoản luôn chỉ được dấy lên từ một beacon hành vi.
Hãy để ý sự lệch nhau mà điều này tạo ra, vì nó ảnh hưởng tới idempotency. Một lần gửi
identification trên production có eventId đúng bằng <requestId>:<phase>, nhưng có
hai lần gửi phá vỡ công thức đó. Một account_takeover là <requestId>:ato — hậu tố ở
đây là chuỗi ato theo đúng nghĩa đen, chứ không phải giá trị của trường phase. Một
lần gửi thử từ dashboard là <requestId>:test, trong khi phase trong phần thân theo
lược đồ 2 vẫn ghi primary — còn phần thân theo lược đồ 1 thì hoàn toàn không có trường
phase, nên header là nơi duy nhất hậu tố đó xuất hiện. Hãy dùng thẳng eventId làm
khóa idempotency và đừng bao giờ ráp lại nó từ requestId và phase. Hãy so khớp với
những giá trị bạn xử lý và bỏ qua phần còn lại, thay vì từ chối lần gửi đó.
attack_detected là sự kiện ở cấp workspace: nó không có requestId, không có
visitorId và không có các khối browser, geo, bot hay decision — những khóa
này đơn giản là không tồn tại. account_takeover được sinh ra từ một lượt truy cập cụ
thể và mang theo toàn bộ phần thân nhận diện tương ứng với gói của bạn, cộng thêm khối
accountAlert. Nếu bạn phân tích mọi sự kiện trong cùng một handler, hãy kiểm tra
event trước khi động đến các trường của lượt truy cập.
| Phiên bản | Dành cho ai | Cách chuyển đổi |
|---|---|---|
1 | Các webhook được tạo trước khi có v2 | Vẫn là mặc định đối với chúng |
2 | Các webhook mới | Nút gạt trên thẻ webhook trong dashboard |
Lược đồ v1 đã đóng băng — không trường nào của nó thay đổi, nên các tích hợp hiện có vẫn hoạt động mà không cần chỉnh sửa. Mọi thứ mới đều nằm ở v2, và đó chính là những gì các webhook mới phát ra.
{ "version": 2, "event": "identification", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:primary", // "<requestId>:<phase>" — the idempotency key "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", // visit identifier, shared by all phases "phase": "primary", "visitorId": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "linkedId": "user-42", // your ?lid=, if you passed one "tag": "checkout", "timestamp": "2026-07-30T12:00:00Z", "url": "https://shop.example.com/checkout", "ip": "203.0.113.44", "userAgent": "Mozilla/5.0 …", "browser": { "name": "Chrome", "version": "138" }, "os": { "name": "macOS", "version": "15.5" }, "device": "desktop", "gpu": "Intel Iris Plus Graphics 655", // model của card đồ họa, đã chuẩn hóa; vắng mặt khi không xác định được "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.405, "timezone": "Europe/Berlin" }, "network": { "vpn": false, "proxy": true, "tor": false, "datacenter": true, "connectionType": "DCH", "proxyDetected": true // lưu lượng HTTP và đường mạng thô đi ra qua các mạng khác nhau }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Giá trị bằng không và rỗng bị lược bỏ. Các trường chuỗi và số có giá trị bằng không
(ví dụ bot.type đối với một con người) sẽ không có mặt trong JSON — đừng bắt buộc
chúng trong lược đồ của bạn, và hãy đọc các khối lồng nhau một cách phòng thủ.
bot.score và decision.riskScore là số thập phân trên thang 0..100 với một chữ
số sau dấu thập phân — đúng những con số mà dashboard báo cáo cho cùng lượt truy cập.
(Ở lược đồ v1 đã đóng băng, chúng dùng đơn vị khác: lần lượt là phân số 0..1 và
0..255.)
bot.type là tên của một bot đã được nhận diện hoặc nhãn của một họ. Xem
Các loại bot để biết từ vựng — tên các kiểm tra nội bộ
không bao giờ được phơi bày, trên bất kỳ gói nào.
| Trường | Kiểu | Mô tả |
|---|---|---|
version | number | Phiên bản lược đồ payload (2) |
event | string | Loại sự kiện |
eventId | string | Định danh lần gửi — khóa idempotency |
requestId | string | Định danh lượt truy cập (UUID), dùng chung cho mọi pha của lượt đó |
phase | string | primary, late, correction; account_takeover mang beacon |
visitorId | string | Định danh khách truy cập ổn định |
linkedId | string | Định danh liên kết do máy khách cung cấp |
tag | string | Thẻ tùy chỉnh do máy khách 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 máy khách |
userAgent | string | Chuỗi user-agent thô của máy khách |
browser.name / .version | string | Trình duyệt được nhận diện |
os.name / .version | string | Hệ điều hành được nhận diện |
device | string | Nhóm thiết bị (ví dụ desktop, mobile) |
gpu | string | Model của card đồ họa theo báo cáo của trình duyệt (WebGL), được chuẩn hóa thành tên dễ đọc (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer cho bộ dựng hình bằng phần mềm; vắng mặt khi không xác định được |
geo | object | Định vị theo IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (boolean) và connectionType |
network.proxyDetected | boolean | Lưu lượng HTTP của lượt truy cập và các đường mạng thô của nó đi ra qua các mạng khác nhau — có proxy hoặc VPN đứng trước trình duyệt. Hai địa chỉ của cùng một nhà cung cấp (NAT của nhà mạng, một lối ra thứ hai của cùng VPN) thì không tính |
bot.result | string | human, bot hoặc uncertain |
bot.type | string | Tên bot hoặc nhãn họ khi phát hiện được bot |
bot.score | number | Điểm bot (0–100) |
identification.confidence | number | Độ tin cậy của việc nhận diện (0.0–1.0) |
identification.incognito | boolean | Ngữ cảnh duyệt riêng tư/ẩn danh |
decision.action | string | real, fake hoặc suspicious |
decision.riskScore | number | Điểm rủi ro tổng hợp (0–100) |
Từ Pro trở lên — khách truy cập hành xử ra sao theo thời gian:
{ "identification": { "matchType": "exact", // exact | fuzzy | new — how the visitor was recognized "matchConfidence": 0.93, "visits": 42, "incognitoVisits": 3 }, // Present when visitor counters are available at event time (usually primary). // A missing block means "no data", not "zeros". "velocity": { "events5m": 7, "uniqueIps": 2, "uniqueLocations": 1 }, "bot": { "antidetectScore": 0 }, // antidetect indicators, 0..100 "session": { "durationSeconds": 95 } // where the visit duration is already known}Từ Business trở lên — vì sao phán quyết lại như vậy:
{ "reasons": [ // at most 8, sorted by importance { "code": "headless_browser", "severity": "high" }, { "code": "privacy_hardening", "severity": "low" } ], // Behavioral biometrics — present only when behavioral scoring ran for the // visit. A missing block means "no data", never "nothing suspicious". "behavior": { "score": 87, "verdict": "human", "confidence": 0.92 }, "identification": { "driftScore": 0.31 }, // divergence from the account profile "deviceInfo": { "deviceId": "…", // the physical device across browsers on it "crossBrowser": true, "confidence": 0.88, "linkedBrowsers": 3 }, "network": { "isp": "Deutsche Telekom", "asn": 3320, // địa chỉ công cộng quan sát được trên đường mạng thô, tức là địa chỉ nằm // sau proxy hoặc VPN; vắng mặt khi không quan sát thấy địa chỉ như vậy "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Môi trường desktop được ĐO trên máy Linux ("Mint 22+", "Ubuntu", // "GNOME", "KDE"); User-Agent không thể diễn đạt một bản phân phối. Vắng // mặt khi không xác định được — ở phần lớn lượt truy cập Linux, và ở mọi // lượt không phải Linux. "osEnvironment": "Mint 22+", // Điều lượt truy cập tự khai về mình so với điều các phép kiểm tra độc lập // đo được. Chỉ có mặt khi thực sự phát hiện được giả mạo; trường `real` // rỗng nghĩa là "phép kiểm tra im lặng", không bao giờ là "đã xác nhận". // Với trục `gpu`, `claimed.gpu` mang card được khai báo dưới cùng tên // model dễ đọc như trường `gpu` ở cấp cao nhất. "spoofing": { "detected": true, "claimed": { "os": "Windows 10", "browser": "Chrome 139.0" }, "real": { "os": "macOS" }, "spoofedAxes": ["os", "screen"], // os | gpu | screen | network | browser "anonymousBrowser": { "detected": true, "names": ["Linken Sphere"] } }, // Dữ kiện thiết bị — những gì trình duyệt của khách truy cập báo cáo về cỗ // máy, đã được làm sạch ở phía chúng tôi. screen: độ phân giải, độ sâu màu // và device pixel ratio. locale: ngôn ngữ ưu tiên và múi giờ của chính // trình duyệt — khác với geo.timezone vốn suy ra từ địa chỉ IP; sự lệch // nhau giữa hai giá trị này là dấu hiệu thường gặp của vị trí bị giả mạo. // clientHints: User-Agent Client Hints — kiến trúc và bitness của CPU, // model thiết bị (Android: mã model trong `model`, ví dụ "SM-A556B", và // tên thương mại của nó từ danh sách thiết bị Google Play trong // `deviceName`, ví dụ "Samsung Galaxy A55 5G") và phiên bản nền tảng // chính xác; chỉ các trình duyệt nền Chromium mới báo cáo chúng. Mỗi khối // sẽ vắng mặt khi lượt truy cập không mang theo dữ liệu như vậy, nên hãy // coi từng khối là tùy chọn. "screen": { "width": 2560, "height": 1600, "colorDepth": 30, "pixelRatio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "clientHints": { "architecture": "arm", "bitness": "64", "platformVersion": "15.5.0" }, // Chỉ có mặt khi card đồ họa tự nhận mình là card ảo; hypervisor là một từ // điển đóng (vmware, virtualbox, parallels, qemu, hyperv, bochs, // intel-gvt, vgpu). Khối vắng mặt nghĩa là không có bằng chứng như vậy. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Xem Phát hiện bot để biết từ vựng mã lý do
và ý nghĩa của severity.
guidance mang theo những khuyến nghị "nên làm gì" đã sẵn sàng cho từng điểm tích hợp,
để bạn không phải tự suy ra chính sách từ các điểm số thô:
{ "guidance": { "version": 1, "overall": "review", // the strictest advice across the scenarios "payment": "review", // whether to accept the payment "registration": "challenge", // whether to create the account "login": "challenge", // whether to let them into the account "affiliate": "review", // whether to credit the conversion to the partner "basis": ["risk", "network"] // the axes that determined the advice }}Mọi kịch bản đều bắt đầu ở allow và chỉ đi lên trên thang:
allow → challenge → review → deny. Trong một kịch bản, trục nghiêm ngặt nhất
được kích hoạt sẽ thắng, còn overall là mức nghiêm ngặt nhất trong cả bốn kịch bản.
| Khuyến nghị | Thanh toán | Đăng ký | Đăng nhập | Affiliate |
|---|---|---|---|---|
allow | Xử lý bình thường | Cứ tạo tài khoản | Cho vào | Ghi nhận chuyển đổi |
challenge | 3-D Secure / bước xác nhận | Captcha, xác nhận qua email hoặc điện thoại | 2FA nâng cấp, xác thực lại | Đánh dấu là đáng ngờ cho tới khi có hoạt động |
review | Xử lý, nhưng đưa vào hàng đợi rà soát | Tạo nhưng kèm hạn chế | Cho vào, đồng thời bật cảnh báo | Giữ khoản chi trả cho tới khi rà soát xong |
deny | Không xử lý giao dịch | Từ chối tạo tài khoản | Không cho vào | Không ghi nhận chuyển đổi |
version là phiên bản của bộ quy tắc — nó được tăng lên khi logic được cải thiện.
Guidance mang tính bổ sung: các kịch bản mới đến dưới dạng khóa mới mà không phá vỡ hợp
đồng. Pha đến sau sẽ thắng, ngoại trừ khuyến nghị một phần: lần gửi được tính trên
tập đầu vào chưa đầy đủ sẽ được đánh dấu "partial": true, và khuyến nghị một phần
không ghi đè lên khuyến nghị đầy đủ đã nhận trước đó cho cùng requestId. Trong
một lần gửi thông thường, trường partial hoàn toàn không xuất hiện.
Các ngưỡng chính xác cố ý không được ghi trong tài liệu. Một khuyến nghị có thể bị suy ngược thành điểm số thì không còn là biện pháp phòng vệ nữa.
account_takeoverChỉ dành cho Business và Enterprise. Phần thân là toàn bộ phong bì nhận diện tương ứng
với gói của bạn, cộng thêm khối accountAlert, được gửi nhiều nhất một lần cho mỗi lượt
truy cập:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // cảnh báo được dấy lên từ beacon; chỉ eventId nói "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}Ở v1, khối này mang type, linkedId và drift; ở v2, hai trường được đổi tên —
linkedId → accountId và drift → driftScore. Hãy cập nhật handler khi bạn chuyển
payloadVersion, nếu không logic chiếm đoạt tài khoản của bạn sẽ âm thầm ngừng nhìn
thấy dữ liệu.
attack_detected{ "version": 2, "event": "attack_detected", "eventId": "c0a8e1f2-…", "timestamp": "2026-07-30T12:00:00Z", "attack": { "kind": "bot_spike", "severity": "critical", // info | warning | critical "windowMinutes": 15, "recentBots": 4210, "recentTotal": 5100, "expected": 180.5 // the baseline expected over a window this size }}Mỗi lần gửi đều kèm header X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t là dấu thời gian Unix (tính bằng giây) tại thời điểm yêu cầu được ký.v1 là HMAC-SHA256 của "<t>.<rawRequestBody>" được mã hóa hex, với khóa là secret
webhook của bạn.Dấu thời gian là một phần của nội dung được ký, nhờ đó có được khả năng chống replay.
Có hai điều phải làm đúng, nếu không việc xác minh sẽ thất bại trên môi trường production:
v1= nào. Trong thời gian xoay vòng
secret, header mang hai chữ ký, và một bộ phân tích chỉ giữ lại một trong số đó sẽ
từ chối những lần gửi hợp lệ suốt cả cửa sổ xoay vòng.// Express.js exampleimport express from "express"import crypto from "crypto"
const app = express()
// Capture the raw body so the signature can be verified 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 { if (!header) return false
const parts = header.split(",").map((p) => p.trim()) const ts = parts.find((p) => p.startsWith("t="))?.slice(2) if (!ts) return false
// Replay protection: reject timestamps more than five minutes old. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
// Sign the raw bytes: the "<t>." prefix plus the raw request body. const signed = Buffer.concat([Buffer.from(`${ts}.`, "utf8"), rawBody]) const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex") const exp = Buffer.from(expected, "hex")
// During a rotation window the header carries several v1= — any may match. return parts.some((p) => { if (!p.startsWith("v1=")) return false const got = Buffer.from(p.slice(3), "hex") // Compare lengths BEFORE timingSafeEqual: it throws on differing lengths, // and one junk header would turn the handler into a 500. return got.length === exp.length && crypto.timingSafeEqual(got, exp) })}
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")})Các lần gửi theo lược đồ 2 còn mang thêm X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Cả hai phía đều biết secret HMAC, nên HMAC chứng
minh rằng người gửi biết secret chứ không chứng minh rằng yêu cầu bắt nguồn từ TRACIO;
chữ ký bất đối xứng làm được điều đó. Khóa công khai được công bố tại
https://api.tracio.ai/.well-known/webhook-keys, tra theo kid.
Các lần gửi thử từ dashboard chỉ được ký bằng HMAC — khóa riêng của nền tảng nằm trên
các node gửi và cố ý không được cấp cho dashboard. Bộ xác minh bắt buộc phải có
Ed25519 thì phải cho các lần gửi thử đi qua (chúng mang hậu tố :test trên
eventId), nếu không việc thử từ dashboard sẽ thất bại trong khi production vẫn khỏe
mạnh. Cũng phải thận trọng tương tự với các kiểm tra định dạng: một lần gửi thử mang
requestId ở dạng test_<hex> và visitorId là chuỗi test_visitor theo đúng nghĩa
đen, nên một handler đối chiếu chúng với khuôn dạng của production sẽ từ chối một lần
gửi vốn hoàn toàn hợp lệ.
Sau khi xoay vòng, cả hai secret vẫn hợp lệ trong 24 giờ và header mang cả hai chữ ký, nhờ đó bạn có thể cập nhật cấu hình mà không mất lần gửi nào. Thao tác Revoke now rút ngắn cửa sổ này. Hãy cập nhật secret ở phía bạn trong vòng 24 giờ: khi cửa sổ khép lại, secret cũ ngừng khớp, và nếu endpoint của bạn trả về 4xx cho chữ ký không hợp lệ thì năm phản hồi như vậy liên tiếp sẽ vô hiệu hóa webhook.
| Header | Mô tả |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — hai v1= trong cửa sổ xoay vòng |
X-Tracio-Signature-Ed25519 | Chữ ký nền tảng, t=<unix>,kid=<id>,v1=<base64> (chỉ v2) |
X-Tracio-Event-Id | Định danh lần gửi — khóa idempotency |
X-Tracio-Request-Id | Định danh lượt truy cập (v2, chỉ với sự kiện của lượt truy cập) |
X-Tracio-Event-Type | Loại sự kiện (chỉ v2) |
X-Tracio-Delivery-Attempt | Số thứ tự lần thử, bắt đầu từ 1 (chỉ v2) |
X-Tracio-Payload-Version | 2 (chỉ v2) |
X-Tracio-Webhook-Id | Định danh của webhook đã tạo ra lần gửi này |
Các lần gửi có thể được thử lại, và lần thử lại mang cùng X-Tracio-Event-Id. Hãy khử
trùng lặp dựa trên giá trị đó:
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")})Lưu ý rằng eventId là duy nhất cho mỗi sự kiện, chứ không phải cho mỗi webhook:
nếu nhiều webhook trong workspace cùng đăng ký một sự kiện, mỗi webhook sẽ nhận được
một lần gửi với cùng định danh đó. Nó được dựng theo dạng <requestId>:<phase>, nhờ
vậy ba pha của cùng một lượt truy cập được khử trùng lặp độc lập với nhau chứ không gộp
lại thành một.
Hãy trả về 2xx — đó là dấu hiệu duy nhất cho thấy lần gửi đã được chấp nhận.
| Phản hồi | Điều gì xảy ra |
|---|---|
2xx | Gửi hoàn tất |
429 Too Many Requests | Không tính là thất bại và không tiêu tốn một lần thử; Retry-After dài hơn sẽ được tôn trọng |
408, 425, 5xx, mất kết nối | Thử lại với khoảng nghỉ tăng dần |
410 Gone | Endpoint được coi là đã bị gỡ bỏ — webhook bị vô hiệu hóa ngay lập tức |
Các mã 4xx khác | Vẫn thử lại, nhưng năm lần liên tiếp sẽ vô hiệu hóa webhook — 400/401/404 không thể chữa bằng cách thử lại |
Lịch thử lại: 5s → 30s → 2min → 10min → 30min → 2h → 6h (8 lần thử). Những lần thử lại đầu tiên nằm gọn trong một phút, nên việc khởi động lại dịch vụ trong chốc lát không khiến bạn mất thông báo. Mỗi khoảng nghỉ được ngẫu nhiên hóa trong khoảng từ một nửa đến trọn giá trị, để các lần thử lại không đồng loạt dội về sau một sự cố.
Tự động vô hiệu hóa đòi hỏi đồng thời một ngưỡng (20 lần thất bại liên tiếp, hoặc 5 lỗi cấu hình) và ít nhất 15 phút thất bại liên tục — một lần khởi động lại ngắn không thể giết chết tích hợp, kể cả khi nhiều lần gửi đã dồn trong hàng đợi. Khoảng trống dài hơn 15 phút sẽ khởi động lại bộ đếm. Dashboard hiển thị lý do kèm mã phản hồi và nội dung lỗi, cùng nút Re-enable để đặt lại các bộ đếm.
| Gói | Số webhook trên mỗi workspace |
|---|---|
| Free | Không khả dụng |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoint phải là https với IP công khai — địa chỉ riêng và loopback đều bị từ chối,
kể cả khi xuất hiện sau một lần chuyển hướng — và không sâu quá hai lần chuyển hướng.
Chỉ chuyển hướng 307 và 308 được đi theo. 301, 302 và 303 yêu cầu máy
khách chuyển sang GET và bỏ phần thân, nên lần gửi sẽ không đi theo chúng và lần thử đó
bị tính là thất bại. Nếu bộ cân bằng tải của bạn chuẩn hóa URL (thêm www hoặc dấu gạch
chéo ở cuối), hãy trỏ webhook thẳng tới URL cuối cùng.
Webhook được quản lý trong dashboard. Dashboard vận hành một API quản trị trong
phạm vi workspace, được phục vụ trên host ứng dụng (ví dụ
https://app.tracio.ai/api/v1), và các endpoint bên dưới chính là những gì nó gọi.
Toàn bộ endpoint webhook nằm dưới /workspaces/{wsId}.
Đây không phải bề mặt dành cho giao tiếp giữa các máy chủ. API quản trị chỉ chấp nhận JWT phiên dashboard của bạn, được đối chiếu với vai trò của bạn trong workspace (RBAC); khóa bí mật dạng
tracio_sk_…bị từ chối ở đây. Vì phiên đó sống trong trình duyệt và hết hạn cùng với nó, hãy xem các lệnh gọi bên dưới như một mô tả về những gì dashboard làm, chứ không phải một tích hợp để tự động hóa. Để truy cập bằng chương trình từ backend của riêng bạn, hãy dùng Data API chỉ đọc.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{wsId}/webhooks \ -H "Authorization: Bearer <session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'Secret ký do TRACIO sinh ra và chỉ được trả về một lần khi tạo (và khi xoay vòng)
trong trường signingSecret. Hãy lưu trữ an toàn — đó là khóa bạn dùng để xác minh chữ
ký.
{ "ok": true, "data": { "id": "b3d4f8a1-2c67-4e9b-8f05-7a1d3c9e2b48", "workspaceEnvironmentId": "b201f2ba-…", "url": "https://your-server.com/webhook/tracio", "events": [], "signingSecret": "f3a9…<hex>", "status": "active", "successRate": 100, "createdAt": "2026-07-30T12:00:00Z" }}Ở những lần đọc sau, signingSecret bị che (null) — nó chỉ được tiết lộ khi tạo và
khi xoay vòng secret.
| Phương thức | Đường dẫn | Mô tả |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Liệt kê webhook |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Cập nhật url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Xóa một webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Gửi một lần gửi thử có chữ ký |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Xoay vòng secret ký |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Liệt kê các lần gửi gần đây |
Hãy trả về 2xx sớm nhất có thể và xử lý payload bất đồng bộ để tránh hết thời gian
chờ:
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 có chữ ký tới endpoint của bạn và xác nhận rằng nó có thể truy cập
được và đang xác minh chữ ký đúng cách.
Khi phát triển cục bộ, hãy phơi máy chủ của bạn qua một tunnel như ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint