Los webhooks entregan eventos de identificación a su servidor en tiempo real. Cada vez que se
identifica a un visitante, TRACIO envía una solicitud HTTP POST a la URL de webhook
que haya configurado. El cuerpo de la solicitud es el payload del evento: no hay ningún
sobre que lo envuelva.
El cuerpo es un documento JSON plano en camelCase. Es un subconjunto público y curado del
evento interno, sin hashes de huella digital ni marcadores de detección internos.
{ "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 | Descripción |
|---|---|---|
requestId | string | Identificador único del evento (también una clave de idempotencia) |
phase | string | primary o late; ver más abajo |
visitorId | string | Identificador de visitante estable |
linkedId | string | Identificador vinculado proporcionado por el cliente |
tag | string | Etiqueta personalizada proporcionada por el cliente |
timestamp | string | Hora del evento (RFC 3339) |
url | string | URL de la página donde se capturó el evento |
ip | string | Dirección IP del cliente |
userAgent | string | Cadena de user-agent sin procesar del cliente |
browser.name / .version | string | Navegador detectado |
os.name / .version | string | Sistema operativo detectado |
device | string | Clase de dispositivo (p. ej. desktop, mobile) |
geo | object | Geolocalización por IP: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (booleanos) y connectionType |
bot.result | string | human, bot o uncertain |
bot.type | string | Etiqueta de automatización de forma libre cuando se detecta un bot |
bot.score | number | Puntuación de probabilidad de bot |
identification.confidence | number | Confianza del modelo (0.0–1.0) |
identification.incognito | boolean | Contexto de navegación privada/incógnito |
identification.visitType | string | Clasificación de la visita |
decision.action | string | Acción recomendada |
decision.riskScore | number | Puntuación de riesgo agregada (0–100) |
decision.suspectScore | number | Puntuación de sospecha detallada |
phase distingue dos entregas que comparten el mismo requestId:
primary — enviado inmediatamente cuando se identifica al visitante.late — un evento de enriquecimiento posterior (un veredicto de bot refinado y
señales de comportamiento capturadas un poco más tarde).Correlacione ambos por requestId y distíngalos por phase.
Cada entrega incluye un encabezado X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t es la marca de tiempo Unix (segundos) del momento en que se firmó la solicitud.v1 es el HMAC-SHA256 codificado en hexadecimal de "<t>.<rawRequestBody>", con clave
su secreto de webhook.La marca de tiempo forma parte del contenido firmado, lo que proporciona protección contra replay:
verifique contra el Buffer sin procesar del cuerpo de la solicitud (no use el JSON
parseado/reserializado, o la firma no coincidirá). Construya el contenido firmado
como bytes: el prefijo "<t>." concatenado con el buffer del cuerpo sin procesar, y luego calcule
su 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")})También podría querer rechazar las entregas cuyo t esté demasiado lejos de la hora
actual (por ejemplo, más de cinco minutos de desfase) como protección adicional contra replay.
| Encabezado | Descripción |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> sobre "<t>.<rawBody>" |
X-Tracio-Event-Id | El requestId del evento; úselo como clave de idempotencia |
X-Tracio-Webhook-Id | Identificador del webhook que produjo esta entrega |
La forma más sencilla de gestionar los webhooks es visualmente en el dashboard. También
pueden gestionarse programáticamente a través de la API de gestión con alcance de workspace,
la misma API que usa el dashboard. Se sirve en el host de la aplicación (por
ejemplo https://app.tracio.ai/api/v1) y se autentica con el JWT de su sesión de
dashboard (Clerk); además, cada solicitud se verifica contra su rol de
workspace (RBAC). No hay ningún secreto de API independiente. Todos los endpoints de webhook
viven bajo /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": [] }'El secreto de firma lo genera TRACIO y se devuelve una sola vez en la creación
(y en la rotación) bajo signingSecret. Guárdelo de forma segura: es la clave que usa
para verificar las firmas.
{ "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" }}En lecturas posteriores, el signingSecret queda enmascarado (null): solo se revela
en la creación y en la rotación del secreto.
| Método | Ruta | Descripción |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Listar webhooks |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | Actualizar url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Eliminar un webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | Enviar una entrega de prueba firmada |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | Rotar el secreto de firma |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Listar los intentos de entrega recientes |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Listar los intentos de entrega recientes |
Las entregas pueden reintentarse, así que su manejador debe ser idempotente. Deduplique por
el encabezado X-Tracio-Event-Id (el 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")})Devuelva una respuesta 2xx lo más rápido posible y procese el payload
de forma asíncrona para evitar timeouts. Las respuestas que no son 2xx (y los errores de conexión)
se reintentan; los fallos repetidos pueden desactivar automáticamente el 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) }}Use la acción Test en un webhook (o POST .../webhooks/:webhookId/test)
para enviar un payload de muestra firmado a su endpoint y confirmar que es accesible y que
verifica las firmas correctamente.
Para desarrollo local, exponga su servidor con un túnel como ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint