Webhook-urile livrează evenimente de identificare pe serverul dumneavoastră în timp
real. De fiecare dată când un vizitator este identificat, TRACIO trimite o cerere
HTTP POST către URL-ul de webhook configurat. Corpul cererii este payload-ul
evenimentului — nu există niciun înveliș (envelope) suplimentar.
Corpul este un document JSON plat, în camelCase. Este un subset public, curatoriat,
al evenimentului intern — fără hash-uri de amprentă (fingerprint) și fără markeri
interni de detecție.
{ "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 }}| Câmp | Tip | Descriere |
|---|---|---|
requestId | string | Identificator unic al evenimentului (și cheie de idempotență) |
phase | string | primary sau late — vezi mai jos |
visitorId | string | Identificator stabil al vizitatorului |
linkedId | string | Identificator asociat furnizat de client |
tag | string | Etichetă personalizată furnizată de client |
timestamp | string | Momentul evenimentului (RFC 3339) |
url | string | URL-ul paginii unde a fost capturat evenimentul |
ip | string | Adresa IP a clientului |
userAgent | string | Șirul brut user-agent al clientului |
browser.name / .version | string | Browserul detectat |
os.name / .version | string | Sistemul de operare detectat |
device | string | Clasa dispozitivului (de ex. desktop, mobile) |
geo | object | Geolocație IP: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (booleeni) și connectionType |
bot.result | string | human, bot sau uncertain |
bot.type | string | Etichetă liberă de automatizare când este detectat un bot |
bot.score | number | Scor de probabilitate a botului |
identification.confidence | number | Încrederea modelului (0.0–1.0) |
identification.incognito | boolean | Context de navigare privată/incognito |
identification.visitType | string | Clasificarea vizitei |
decision.action | string | Acțiunea recomandată |
decision.riskScore | number | Scor de risc agregat (0–100) |
decision.suspectScore | number | Scor de suspiciune granular |
phase distinge două livrări care partajează același requestId:
primary — trimis imediat ce vizitatorul este identificat.late — un eveniment ulterior de îmbogățire (un verdict de bot rafinat și
semnale comportamentale capturate puțin mai târziu).Corelați-le prin requestId și distingeți-le prin phase.
Fiecare livrare include un antet X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t este timestamp-ul Unix (secunde) la care a fost semnată cererea.v1 este HMAC-SHA256 codificat hexazecimal al "<t>.<rawRequestBody>", cu cheia
fiind secretul webhook-ului dumneavoastră.Timestamp-ul face parte din conținutul semnat, ceea ce oferă protecție împotriva
reluării (replay) — verificați folosind Buffer-ul brut al corpului cererii
(nu folosiți JSON-ul parsat/re-serializat, altfel semnătura nu va corespunde).
Construiți conținutul semnat ca octeți: prefixul "<t>." concatenat cu buffer-ul
brut al corpului, apoi aplicați 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")})De asemenea, este recomandat să respingeți livrările al căror t este prea departe
de ora curentă (de exemplu, o abatere mai mare de cinci minute) ca o măsură
suplimentară împotriva reluării.
| Antet | Descriere |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> peste "<t>.<rawBody>" |
X-Tracio-Event-Id | requestId-ul evenimentului — folosiți-l ca cheie de idempotență |
X-Tracio-Webhook-Id | Identificatorul webhook-ului care a produs această livrare |
Cea mai simplă modalitate de a gestiona webhook-urile este vizual, în dashboard.
Ele pot fi gestionate și programatic, prin API-ul de administrare cu domeniu la
nivel de workspace — același API pe care îl folosește și dashboard-ul. Este servit
pe host-ul aplicației (de exemplu https://app.tracio.ai/api/v1) și autentificat
cu JWT-ul sesiunii de dashboard (Clerk); fiecare cerere este verificată în plus
față de rolul dumneavoastră în workspace (RBAC). Nu există niciun secret de API
autonom. Toate endpoint-urile de webhook se află sub /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": [] }'Secretul de semnare este generat de TRACIO și returnat o singură dată la creare
(și la rotire) sub signingSecret. Păstrați-l în siguranță — este cheia pe care o
folosiți pentru a verifica semnăturile.
{ "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" }}La citirile ulterioare, signingSecret este mascat (null) — este dezvăluit doar
la creare și la rotirea secretului.
| Metodă | Cale | Descriere |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Listează webhook-urile |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | Actualizează url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Șterge un webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | Trimite o livrare de test semnată |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | Rotește secretul de semnare |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Listează încercările recente de livrare |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Listează încercările recente de livrare |
Livrările pot fi reîncercate, așadar handler-ul dumneavoastră ar trebui să fie
idempotent. Deduplicați pe antetul X-Tracio-Event-Id (adică 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")})Returnați un răspuns 2xx cât mai repede posibil și procesați payload-ul asincron,
pentru a evita timeout-urile. Răspunsurile care nu sunt 2xx (și erorile de
conexiune) sunt reîncercate; eșecurile repetate pot dezactiva automat webhook-ul.
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) }}Folosiți acțiunea Test de pe un webhook (sau POST .../webhooks/:webhookId/test)
pentru a trimite un payload de exemplu semnat către endpoint-ul dumneavoastră și a
confirma că este accesibil și că verifică semnăturile corect.
Pentru dezvoltare locală, expuneți serverul printr-un tunel precum ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint