I webhook recapitano gli eventi di identificazione al suo server in tempo reale. Ogni volta che un
visitatore viene identificato, TRACIO invia una richiesta HTTP POST all'URL del webhook
che ha configurato. Il corpo della richiesta è il payload dell'evento — non c'è alcun
envelope di incapsulamento.
Il corpo è un documento JSON piatto in camelCase. È un sottoinsieme pubblico curato
dell'evento interno — nessun hash di fingerprint né marcatore di rilevamento interno.
{ "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 }}| Campo | Tipo | Descrizione |
|---|---|---|
requestId | string | Identificatore univoco dell'evento (anche chiave di idempotenza) |
phase | string | primary o late — vedi sotto |
visitorId | string | Identificatore stabile del visitatore |
linkedId | string | Identificatore collegato fornito dal client |
tag | string | Tag personalizzato fornito dal client |
timestamp | string | Ora dell'evento (RFC 3339) |
url | string | URL della pagina in cui l'evento è stato catturato |
ip | string | Indirizzo IP del client |
userAgent | string | Stringa user-agent grezza del client |
browser.name / .version | string | Browser rilevato |
os.name / .version | string | Sistema operativo rilevato |
device | string | Classe del dispositivo (es. desktop, mobile) |
geo | object | Geolocalizzazione IP: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (booleani) e connectionType |
bot.result | string | human, bot o uncertain |
bot.type | string | Etichetta di automazione in formato libero quando viene rilevato un bot |
bot.score | number | Punteggio di probabilità del bot |
identification.confidence | number | Confidenza del modello (0.0–1.0) |
identification.incognito | boolean | Contesto di navigazione privata/incognito |
identification.visitType | string | Classificazione della visita |
decision.action | string | Azione consigliata |
decision.riskScore | number | Punteggio di rischio aggregato (0–100) |
decision.suspectScore | number | Punteggio di sospetto a grana fine |
phase distingue due recapiti che condividono lo stesso requestId:
primary — inviato immediatamente quando il visitatore viene identificato.late — un evento di arricchimento di follow-up (un verdetto sui bot raffinato e
segnali comportamentali catturati poco dopo).Correli i due tramite requestId e li distingua tramite phase.
Ogni recapito include un header X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t è il timestamp Unix (secondi) di quando la richiesta è stata firmata.v1 è l'HMAC-SHA256 codificato in esadecimale di "<t>.<rawRequestBody>", con chiave
il suo webhook secret.Il timestamp fa parte del contenuto firmato, il che offre protezione dai replay —
verifichi rispetto al Buffer del corpo della richiesta grezzo (non usi il JSON parsato/
ri-serializzato, altrimenti la firma non corrisponderà). Costruisca il contenuto firmato
come byte: il prefisso "<t>." concatenato con il buffer del corpo grezzo, poi ne calcoli l'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")})Potrebbe anche voler rifiutare i recapiti il cui t è troppo lontano dall'ora
corrente (ad esempio, più di cinque minuti di scarto) come protezione aggiuntiva dai replay.
| Header | Descrizione |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> su "<t>.<rawBody>" |
X-Tracio-Event-Id | Il requestId dell'evento — lo usi come chiave di idempotenza |
X-Tracio-Webhook-Id | Identificatore del webhook che ha prodotto questo recapito |
Il modo più semplice per gestire i webhook è visivamente nella dashboard. Possono
anche essere gestiti in modo programmatico tramite l'API di gestione con ambito workspace —
la stessa API usata dalla dashboard. È servita sull'host dell'applicazione (ad
esempio https://app.tracio.ai/api/v1) e autenticata con il JWT della sua sessione della
dashboard (Clerk); ogni richiesta viene inoltre verificata rispetto al suo ruolo nel
workspace (RBAC). Non esiste un API secret autonomo. Tutti gli endpoint dei webhook
risiedono sotto /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": [] }'Il signing secret è generato da TRACIO e restituito una sola volta alla creazione
(e alla rotazione) sotto signingSecret. Lo conservi in modo sicuro — è la chiave che usa
per verificare le firme.
{ "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" }}Nelle letture successive il signingSecret è mascherato (null) — viene rivelato solo
dalla creazione e dalla rotazione del secret.
| Metodo | Percorso | Descrizione |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Elenca i webhook |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | Aggiorna url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Elimina un webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | Invia un recapito di test firmato |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | Ruota il signing secret |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Elenca i tentativi di recapito recenti |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Elenca i tentativi di recapito recenti |
I recapiti possono essere ritentati, quindi il suo handler dovrebbe essere idempotente. Deduplichi
sull'header X-Tracio-Event-Id (il 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")})Restituisca una risposta 2xx il più velocemente possibile ed elabori il payload
in modo asincrono per evitare timeout. Le risposte non-2xx (e gli errori di connessione)
vengono ritentate; fallimenti ripetuti possono disabilitare automaticamente il 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) }}Usi l'azione Test su un webhook (o POST .../webhooks/:webhookId/test)
per inviare un payload di esempio firmato al suo endpoint e confermare che sia raggiungibile e
che verifichi correttamente le firme.
Per lo sviluppo in locale, esponga il suo server con un tunnel come ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint