Les webhooks livrent les événements d'identification à votre serveur en temps réel.
Chaque fois qu'un visiteur est identifié, TRACIO envoie une requête HTTP POST à
l'URL de webhook que vous avez configurée. Le corps de la requête est le payload
de l'événement — il n'y a pas d'enveloppe englobante.
Le corps est un document JSON plat en camelCase. C'est un sous-ensemble public
soigneusement sélectionné de l'événement interne — sans hachage d'empreinte ni
marqueur de détection interne.
{ "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 }}| Champ | Type | Description |
|---|---|---|
requestId | string | Identifiant unique de l'événement (aussi une clé d'idempotence) |
phase | string | primary ou late — voir ci-dessous |
visitorId | string | Identifiant visiteur stable |
linkedId | string | Identifiant lié fourni par le client |
tag | string | Tag personnalisé fourni par le client |
timestamp | string | Heure de l'événement (RFC 3339) |
url | string | URL de la page où l'événement a été capturé |
ip | string | Adresse IP du client |
userAgent | string | Chaîne user-agent brute du client |
browser.name / .version | string | Navigateur détecté |
os.name / .version | string | Système d'exploitation détecté |
device | string | Classe d'appareil (par ex. desktop, mobile) |
geo | object | Géolocalisation IP : country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (booléens) et connectionType |
bot.result | string | human, bot ou uncertain |
bot.type | string | Étiquette d'automatisation libre lorsqu'un bot est détecté |
bot.score | number | Score de probabilité de bot |
identification.confidence | number | Confiance du modèle (0.0–1.0) |
identification.incognito | boolean | Contexte de navigation privée/incognito |
identification.visitType | string | Classification de la visite |
decision.action | string | Action recommandée |
decision.riskScore | number | Score de risque agrégé (0–100) |
decision.suspectScore | number | Score de suspicion fin |
phase distingue deux livraisons qui partagent le même requestId :
primary — envoyée immédiatement lorsque le visiteur est identifié.late — un événement d'enrichissement de suivi (un verdict de bot affiné et
des signaux comportementaux capturés un peu plus tard).Corrélez les deux par requestId et distinguez-les par phase.
Chaque livraison inclut un en-tête X-Tracio-Signature :
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t est le timestamp Unix (secondes) au moment où la requête a été signée.v1 est le HMAC-SHA256 encodé en hexadécimal de "<t>.<rawRequestBody>", avec
votre secret de webhook comme clé.Le timestamp fait partie du contenu signé, ce qui offre une protection contre le
rejeu — vérifiez par rapport au Buffer brut du corps de la requête (n'utilisez
pas le JSON analysé/re-sérialisé, sinon la signature ne correspondra pas). Construisez
le contenu signé sous forme d'octets : le préfixe "<t>." concaténé avec le buffer
du corps brut, puis calculez le HMAC de cela.
// 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")})Vous pouvez aussi rejeter les livraisons dont le t est trop éloigné de l'heure
actuelle (par exemple, plus de cinq minutes de décalage) comme protection
supplémentaire contre le rejeu.
| En-tête | Description |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> sur "<t>.<rawBody>" |
X-Tracio-Event-Id | Le requestId de l'événement — à utiliser comme clé d'idempotence |
X-Tracio-Webhook-Id | Identifiant du webhook qui a produit cette livraison |
La manière la plus simple de gérer les webhooks est visuellement dans le tableau de
bord. Ils peuvent aussi être gérés par programmation via l'API de gestion à portée
d'espace de travail — la même API que celle utilisée par le tableau de bord. Elle est
servie sur l'hôte de l'application (par exemple https://app.tracio.ai/api/v1) et
authentifiée avec le JWT de votre session de tableau de bord (Clerk) ; chaque
requête est en outre contrôlée par rapport à votre rôle dans l'espace de travail (RBAC).
Il n'y a pas de secret d'API autonome. Tous les endpoints de webhook se trouvent sous
/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": [] }'Le secret de signature est généré par TRACIO et renvoyé une seule fois à la
création (et lors de la rotation) sous signingSecret. Stockez-le en toute sécurité —
c'est la clé que vous utilisez pour vérifier les signatures.
{ "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" }}Lors des lectures suivantes, le signingSecret est masqué (null) — il n'est révélé
que par la création et la rotation du secret.
| Méthode | Chemin | Description |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | Lister les webhooks |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | Mettre à jour url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | Supprimer un webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | Envoyer une livraison de test signée |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | Faire tourner le secret de signature |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Lister les tentatives de livraison récentes |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | Lister les tentatives de livraison récentes |
Les livraisons peuvent être réessayées, votre gestionnaire doit donc être idempotent.
Dédupliquez sur l'en-tête X-Tracio-Event-Id (le 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")})Renvoyez une réponse 2xx le plus vite possible et traitez le payload de manière
asynchrone pour éviter les timeouts. Les réponses non 2xx (et les erreurs de
connexion) sont réessayées ; des échecs répétés peuvent désactiver automatiquement
le 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) }}Utilisez l'action Test sur un webhook (ou POST .../webhooks/:webhookId/test)
pour envoyer un exemple de payload signé à votre endpoint et confirmer qu'il est
joignable et vérifie correctement les signatures.
Pour le développement local, exposez votre serveur avec un tunnel tel que ngrok :
ngrok http 3000# Use the generated URL as your webhook endpoint