Webhook mengirimkan peristiwa identifikasi ke server Anda secara real-time. Setiap
kali seorang pengunjung teridentifikasi, TRACIO mengirim permintaan HTTP POST ke
URL webhook yang Anda konfigurasikan. Body permintaan itu adalah payload
peristiwanya.
Webhook juga satu-satunya kanal yang mengirimkan verdict terlambat — yaitu verdict ketika perilaku pengunjung membuktikan bahwa ia otomatis setelah halaman sudah dimuat.
Aturlah di dashboard pada Settings → Webhooks. Webhook memerlukan paket Pro atau lebih tinggi.
| Peristiwa | Kapan | Paket |
|---|---|---|
identification | Pada setiap kunjungan — fase primary, late, dan correction | Semua |
account_takeover | Perilaku pada sebuah akun tidak lagi cocok dengan profil pemiliknya | Business ke atas |
attack_detected | Lonjakan bot di situs Anda | Business ke atas |
reputation_changed | Reputasi orang di balik sebuah perangkat berubah | Business ke atas |
Nama peristiwa memakai garis bawah, tidak pernah titik — tidak ada visitor.created
maupun session.created. reputation_changed memerlukan lapisan person, jadi
peristiwa itu hanya terpicu untuk workspace yang mengaktifkan resolusi identitas
lintas perangkat.
Sebuah webhook berlangganan tipe tertentu; nilai terpisah * berarti "setiap tipe,
termasuk yang ditambahkan kemudian". Tipe yang tidak dikenal ditolak dengan 400
saat langganan dibuat atau diedit, sehingga salah ketik tidak akan meninggalkan Anda
dengan webhook yang diam-diam tidak pernah terpicu.
identificationSatu kunjungan menghasilkan hingga tiga pengiriman yang berbagi requestId yang
sama:
primary — verdict awal, saat halaman dimuat.late — pengayaan sekitar sembilan detik kemudian, setelah pemeriksaan yang lambat masuk.correction — koreksi berdasarkan perilaku (pointer, ketikan, scrolling).Korelasikan dengan requestId dan bedakan dengan phase. Fase yang lebih akhir
lebih diutamakan: jika primary menyebut human dan correction menyebut bot,
maka yang kedua adalah jawaban yang benar.
Jangan mengandalkan urutan kedatangan. Setiap fase dikirim secara independen dan
dengan jadwal percobaan ulangnya sendiri — jika primary masuk ke percobaan ulang
sementara late berhasil pada percobaan pertama, Anda akan menerimanya dalam urutan
terbalik. Tentukan prioritas dari field phase, bukan dari waktu penerimaan.
Ketiga fase itulah satu-satunya fase peristiwa identification. Satu nilai lain juga
sampai kepada Anda: account_takeover membawa phase: "beacon", karena peringatan
pengambilalihan akun selalu dimunculkan dari beacon perilaku.
Perhatikan ketidakcocokan yang muncul karenanya, sebab hal ini memengaruhi
idempotensi. Pengiriman identification di produksi memiliki eventId yang persis
berbentuk <requestId>:<phase>, tetapi dua jenis pengiriman melanggar rumus itu.
Pada account_takeover, nilainya <requestId>:ato — akhirannya adalah literal ato,
bukan nilai field phase. Pengiriman uji yang dikirim dari dashboard bernilai
<requestId>:test, sementara phase pada body skema 2-nya tetap terbaca primary —
dan body skema 1 tidak memiliki field phase sama sekali, sehingga akhiran itu hanya
muncul di header. Gunakan eventId langsung sebagai kunci idempotensi dan jangan
pernah menyusunnya ulang dari requestId dan phase. Cocokkan pada nilai yang Anda
tangani dan abaikan sisanya alih-alih menolak pengirimannya.
attack_detected adalah peristiwa pada level workspace: tidak memiliki requestId,
tidak memiliki visitorId, dan tidak memiliki blok browser, geo, bot, maupun
decision — kunci-kunci itu memang tidak ada. account_takeover dihasilkan oleh satu
kunjungan tertentu dan membawa body identifikasi lengkap sesuai paket Anda plus blok
accountAlert. Jika Anda mem-parsing setiap peristiwa dalam satu handler, periksa
event sebelum menyentuh field kunjungan.
| Versi | Untuk siapa | Cara beralih |
|---|---|---|
1 | Webhook yang dibuat sebelum v2 ada | Tetap menjadi default bagi mereka |
2 | Webhook baru | Tombol pada kartu webhook di dashboard |
Skema v1 dibekukan — tidak ada field-nya yang berubah, sehingga integrasi yang sudah ada terus bekerja tanpa penyuntingan. Semua yang baru berada di v2, dan itulah yang dikirim webhook baru.
{ "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 adaptor video, dinormalisasi; tidak ada bila tidak diketahui "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 // lalu lintas HTTP dan jalur jaringan mentah keluar lewat jaringan yang berbeda }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Nilai nol dan kosong dihilangkan. Field string dan numerik yang bernilai nol
(misalnya bot.type untuk manusia) tidak ada dalam JSON — jangan menjadikannya wajib
di skema Anda, dan bacalah blok bersarang secara defensif.
bot.score dan decision.riskScore adalah desimal pada skala 0..100 dengan
satu angka di belakang koma — persis angka yang dilaporkan dashboard untuk kunjungan
yang sama. (Pada skema v1 yang dibekukan keduanya memakai satuan berbeda: pecahan
0..1 dan 0..255.)
bot.type berisi nama bot yang dikenali atau label famili. Lihat
Tipe Bot untuk kosakatanya — nama pemeriksaan
internal tidak pernah diekspos, pada paket mana pun.
| Field | Tipe | Deskripsi |
|---|---|---|
version | number | Versi skema payload (2) |
event | string | Tipe peristiwa |
eventId | string | Pengidentifikasi pengiriman — kunci idempotensi |
requestId | string | Pengidentifikasi kunjungan (UUID), dibagikan oleh semua fase kunjungan |
phase | string | primary, late, correction; account_takeover membawa beacon |
visitorId | string | Pengidentifikasi pengunjung yang stabil |
linkedId | string | Pengidentifikasi tertaut yang diberikan klien |
tag | string | Tag kustom yang diberikan klien |
timestamp | string | Waktu peristiwa (RFC 3339) |
url | string | URL halaman tempat peristiwa ditangkap |
ip | string | Alamat IP klien |
userAgent | string | String user-agent klien apa adanya |
browser.name / .version | string | Browser yang terdeteksi |
os.name / .version | string | Sistem operasi yang terdeteksi |
device | string | Kelas perangkat (mis. desktop, mobile) |
gpu | string | Model adaptor video seperti yang dilaporkan peramban (WebGL), dinormalisasi menjadi nama yang terbaca (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer untuk perender perangkat lunak; tidak ada bila tidak diketahui |
geo | object | Geolokasi IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (boolean) dan connectionType |
network.proxyDetected | boolean | Lalu lintas HTTP kunjungan dan jalur jaringan mentahnya keluar lewat jaringan yang berbeda — ada proxy atau VPN di depan peramban. Dua alamat dari penyedia yang sama (NAT operator, keluaran kedua dari VPN yang sama) tidak dihitung |
bot.result | string | human, bot, atau uncertain |
bot.type | string | Nama bot atau label famili ketika bot terdeteksi |
bot.score | number | Skor bot (0–100) |
identification.confidence | number | Keyakinan identifikasi (0.0–1.0) |
identification.incognito | boolean | Konteks penjelajahan pribadi/incognito |
decision.action | string | real, fake, atau suspicious |
decision.riskScore | number | Skor risiko agregat (0–100) |
Pro ke atas — bagaimana pengunjung berperilaku dari waktu ke waktu:
{ "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}Business ke atas — mengapa verdict-nya seperti itu:
{ "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, // alamat publik yang teramati pada jalur jaringan mentah, yaitu alamat di // balik proxy atau VPN; tidak ada bila alamat semacam itu tidak teramati "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Lingkungan desktop yang DIUKUR pada mesin Linux ("Mint 22+", "Ubuntu", // "GNOME", "KDE"); User-Agent tidak dapat menyatakan distribusi. Tidak ada // bila tidak dapat ditentukan — pada sebagian besar kunjungan Linux, dan // pada setiap kunjungan non-Linux. "osEnvironment": "Mint 22+", // Apa yang diklaim kunjungan tentang dirinya dibandingkan dengan apa yang // diukur pemeriksaan independen. Hadir hanya bila pemalsuan benar-benar // terdeteksi; field `real` yang kosong berarti "pemeriksaan tetap diam", // bukan "terkonfirmasi". Pada sumbu `gpu`, `claimed.gpu` membawa adaptor // yang diklaim dengan nama model terbaca yang sama seperti field `gpu` // tingkat atas. "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"] } }, // Fakta perangkat — apa yang dilaporkan peramban pengunjung tentang // mesinnya, sudah dibersihkan di sisi kami. screen: resolusi, kedalaman // warna, dan device pixel ratio. locale: bahasa pilihan dan zona waktu // peramban itu sendiri — berbeda dengan geo.timezone yang diturunkan dari // alamat IP; ketidakcocokan di antara keduanya adalah tanda umum lokasi // yang dipalsukan. clientHints: User-Agent Client Hints — arsitektur dan // bitness CPU, model perangkat (Android: kode model di `model`, mis. // "SM-A556B", dan nama pemasarannya dari daftar perangkat Google Play di // `deviceName`, mis. "Samsung Galaxy A55 5G"), dan versi platform yang // persis; // hanya peramban berbasis Chromium yang melaporkannya. Setiap blok tidak // ada bila kunjungan tidak membawa data semacam itu, jadi perlakukan // masing-masing sebagai opsional. "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" }, // Hadir hanya bila adaptor video menyatakan dirinya sebagai adaptor // virtual; hypervisor adalah kamus tertutup (vmware, virtualbox, // parallels, qemu, hyperv, bochs, intel-gvt, vgpu). Blok yang tidak ada // berarti tidak ada bukti semacam itu. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Lihat Deteksi Bot untuk kosakata kode
alasan dan arti severity.
guidance membawa rekomendasi "apa yang harus dilakukan" yang sudah siap pakai untuk
setiap titik integrasi, sehingga Anda tidak perlu menurunkan kebijakan sendiri dari
skor mentah:
{ "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 }}Setiap skenario dimulai pada allow dan hanya bergerak naik pada tangga:
allow → challenge → review → deny. Di dalam satu skenario, sumbu terketat yang
terpicu yang menang, dan overall adalah yang terketat di antara keempat skenario.
| Saran | Pembayaran | Pendaftaran | Login | Afiliasi |
|---|---|---|---|---|
allow | Proses saja | Buat saja | Izinkan masuk | Kreditkan konversinya |
challenge | 3-D Secure / konfirmasi | Captcha, konfirmasi email atau telepon | 2FA step-up, autentikasi ulang | Tandai meragukan sampai ada aktivitas nyata |
review | Proses, tetapi antrekan untuk ditinjau | Buat dengan pembatasan | Izinkan masuk, tetapi picu alert | Tahan pembayaran sampai ditinjau |
deny | Jangan proses transaksinya | Tolak pembuatan akun | Jangan izinkan masuk | Jangan kreditkan konversinya |
version adalah versi kumpulan aturan — angkanya dinaikkan seiring perbaikan logika.
Guidance bersifat aditif: skenario baru datang sebagai kunci baru tanpa merusak
kontrak. Fase yang lebih akhir menang, kecuali untuk saran parsial: pengiriman
yang dihitung dari kumpulan masukan yang belum lengkap ditandai "partial": true, dan
saran parsial tidak menimpa saran lengkap yang diterima lebih dulu untuk
requestId yang sama. Pada pengiriman biasa, field partial sama sekali tidak ada.
Ambang batas persisnya sengaja tidak didokumentasikan. Saran yang bisa direkayasa balik menjadi skor berhenti menjadi pertahanan.
account_takeoverHanya Business dan Enterprise. Body-nya adalah amplop identifikasi lengkap sesuai
paket Anda plus blok accountAlert, dikirim paling banyak satu kali per kunjungan:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // peringatan dimunculkan dari beacon; hanya eventId yang menyebut "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}Pada v1 blok ini membawa type, linkedId, dan drift; pada v2 dua field diganti
namanya — linkedId → accountId dan drift → driftScore. Perbarui handler Anda
saat Anda mengganti payloadVersion, atau logika pengambilalihan akun Anda akan
diam-diam berhenti melihat datanya.
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 }}Setiap pengiriman menyertakan header X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t adalah timestamp Unix (detik) saat permintaan ditandatangani.v1 adalah HMAC-SHA256 dari "<t>.<rawRequestBody>" dalam bentuk heksadesimal,
dengan kunci berupa secret webhook Anda.Timestamp adalah bagian dari konten yang ditandatangani, dan itulah yang memberi perlindungan terhadap replay.
Ada dua hal yang harus benar, kalau tidak verifikasi akan gagal di produksi:
v1= mana pun. Selama rotasi secret, header
membawa dua tanda tangan, dan parser yang hanya menyimpan salah satunya akan
menolak pengiriman yang sah selama seluruh jendela rotasi.// 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")})Pengiriman skema 2 juga membawa X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Kedua pihak mengetahui secret HMAC, jadi HMAC
membuktikan bahwa pengirim mengetahui secret tersebut, tetapi bukan bahwa permintaan
itu berasal dari TRACIO; tanda tangan asimetris membuktikannya. Kunci publik
dipublikasikan di https://api.tracio.ai/.well-known/webhook-keys, diindeks
berdasarkan kid.
Pengiriman uji yang dikirim dari dashboard hanya ditandatangani dengan HMAC — kunci
privat platform berada di node pengiriman dan sengaja tidak tersedia bagi dashboard.
Verifikator yang mewajibkan Ed25519 harus meloloskan pengiriman uji (pengiriman
uji membawa akhiran :test pada eventId), jika tidak, pengujian dari dashboard akan
gagal padahal produksi sehat. Kehati-hatian yang sama berlaku untuk pemeriksaan
format: pengiriman uji membawa requestId dalam bentuk test_<hex> dan literal
test_visitor sebagai visitorId, sehingga handler yang memvalidasi keduanya
terhadap bentuk produksi akan menolak pengiriman yang sebenarnya sudah benar.
Setelah rotasi, kedua secret tetap valid selama 24 jam dan header membawa kedua tanda tangan, sehingga Anda dapat memperbarui konfigurasi tanpa kehilangan pengiriman. Tindakan Revoke now memperpendek jendela tersebut. Perbarui secret di sisi Anda dalam 24 jam: begitu jendela tertutup, secret lama berhenti cocok, dan jika endpoint Anda menjawab tanda tangan yang tidak valid dengan 4xx, lima jawaban seperti itu berturut-turut akan menonaktifkan webhook.
| Header | Deskripsi |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — dua v1= selama jendela rotasi |
X-Tracio-Signature-Ed25519 | Tanda tangan platform, t=<unix>,kid=<id>,v1=<base64> (khusus v2) |
X-Tracio-Event-Id | Pengidentifikasi pengiriman — kunci idempotensi |
X-Tracio-Request-Id | Pengidentifikasi kunjungan (v2, khusus peristiwa kunjungan) |
X-Tracio-Event-Type | Tipe peristiwa (khusus v2) |
X-Tracio-Delivery-Attempt | Nomor percobaan, dimulai dari 1 (khusus v2) |
X-Tracio-Payload-Version | 2 (khusus v2) |
X-Tracio-Webhook-Id | Pengidentifikasi webhook yang menghasilkan pengiriman ini |
Pengiriman dapat dicoba ulang, dan percobaan ulang membawa X-Tracio-Event-Id yang
sama. Lakukan deduplikasi berdasarkan nilai itu:
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")})Perhatikan bahwa eventId unik per peristiwa, bukan per webhook: jika beberapa
webhook di workspace berlangganan peristiwa yang sama, masing-masing menerima
pengiriman dengan pengidentifikasi yang sama. Nilainya dibangun sebagai
<requestId>:<phase>, itulah sebabnya ketiga fase dari satu kunjungan dideduplikasi
secara terpisah alih-alih menyatu menjadi satu.
Jawablah dengan 2xx — itulah satu-satunya tanda bahwa pengiriman diterima.
| Respons | Apa yang terjadi |
|---|---|
2xx | Pengiriman selesai |
429 Too Many Requests | Tidak dihitung sebagai kegagalan dan tidak menghabiskan percobaan; Retry-After yang lebih panjang dihormati |
408, 425, 5xx, koneksi terputus | Dicoba ulang dengan jeda yang makin panjang |
410 Gone | Endpoint dianggap sudah dihapus — webhook langsung dinonaktifkan |
4xx lainnya | Dicoba ulang, tetapi lima kali berturut-turut menonaktifkan webhook — 400/401/404 tidak sembuh dengan percobaan ulang |
Jadwal percobaan ulang: 5s → 30s → 2min → 10min → 30min → 2h → 6h (8 percobaan). Percobaan pertama masih muat dalam satu menit, jadi restart singkat pada layanan Anda tidak membuat Anda kehilangan notifikasi. Setiap jeda diacak antara separuh dan nilai penuhnya agar percobaan ulang tidak menyerbu serentak setelah gangguan.
Penonaktifan otomatis menuntut ambang batas (20 kegagalan berturut-turut, atau 5 kesalahan konfigurasi) dan setidaknya 15 menit kegagalan berturut-turut — restart singkat tidak dapat mematikan integrasi sekalipun banyak pengiriman menumpuk di antrean. Jeda yang lebih panjang dari 15 menit mengulang hitungannya dari awal. Dashboard menampilkan alasannya, lengkap dengan kode respons dan teks kesalahan, serta tombol Re-enable yang mereset penghitungnya.
| Paket | Webhook per workspace |
|---|---|
| Free | Tidak tersedia |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoint harus https dengan IP publik — alamat privat dan loopback ditolak,
termasuk pada redirect — dan tidak lebih dari dua tingkat redirect.
Hanya redirect 307 dan 308 yang diikuti. 301, 302, dan 303 memerintahkan
klien beralih ke GET dan membuang body, jadi pengiriman tidak mengikutinya dan
percobaan itu dihitung gagal. Jika load balancer Anda menormalkan URL (menambah www
atau garis miring di akhir), arahkan webhook langsung ke URL finalnya.
Webhook dikelola di dashboard. Dashboard menjalankan API manajemen bercakupan
workspace yang dilayani di host aplikasi (misalnya https://app.tracio.ai/api/v1),
dan endpoint di bawah ini persis itulah yang dipanggilnya. Semua endpoint webhook
berada di bawah /workspaces/{wsId}.
Ini bukan permukaan server-ke-server. API manajemen hanya menerima JWT sesi dashboard Anda, yang diperiksa terhadap peran Anda di workspace (RBAC); secret key
tracio_sk_…ditolak di sini. Karena sesi itu hidup di browser dan berakhir bersamanya, perlakukan panggilan di bawah ini sebagai penjelasan tentang apa yang dilakukan dashboard, bukan sebagai integrasi untuk diotomatiskan. Untuk akses terprogram dari backend Anda sendiri, gunakan Data API yang hanya-baca.
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 penandatanganan dihasilkan oleh TRACIO dan dikembalikan satu kali saat
pembuatan (dan saat rotasi) di bawah signingSecret. Simpan dengan aman — itulah
kunci yang Anda gunakan untuk memverifikasi tanda tangan.
{ "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" }}Pada pembacaan berikutnya signingSecret disamarkan (null) — nilainya hanya
diungkap saat pembuatan dan rotasi secret.
| Metode | Path | Deskripsi |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Daftar webhook |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Perbarui url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Hapus webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Kirim pengiriman uji bertanda tangan |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotasi secret penandatanganan |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Daftar percobaan pengiriman terbaru |
Kembalikan 2xx secepat mungkin dan proses payload secara asinkron agar tidak
kehabisan waktu:
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) }}Gunakan tindakan Test pada sebuah webhook (atau
POST .../webhooks/{webhookId}/test) untuk mengirim contoh payload bertanda tangan ke
endpoint Anda dan memastikan endpoint itu dapat dijangkau serta memverifikasi tanda
tangan dengan benar.
Untuk pengembangan lokal, ekspos server Anda dengan tunnel seperti ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint