Webhooks leveren identificatie-events in realtime aan uw server. Elke keer dat een
bezoeker wordt geïdentificeerd, stuurt TRACIO een HTTP-POST-verzoek naar de
webhook-URL die u hebt ingesteld. De body van het verzoek is de event-payload.
Ze zijn bovendien het enige kanaal dat late verdicts levert — die waarbij het gedrag van een bezoeker pas ná het laden van de pagina aantoonde dat hij geautomatiseerd was.
Stel ze in het dashboard in onder Settings → Webhooks. Webhooks vereisen het Pro-abonnement of hoger.
| Event | Wanneer | Abonnement |
|---|---|---|
identification | Bij elk bezoek — de fasen primary, late en correction | Alle |
account_takeover | Gedrag onder een account komt niet langer overeen met het profiel van de eigenaar | Business+ |
attack_detected | Een piek aan bots op uw site | Business+ |
reputation_changed | De reputatie van de persoon achter een apparaat is gewijzigd | Business+ |
Event-namen gebruiken underscores, nooit punten — er bestaat geen visitor.created of
session.created. reputation_changed vereist de personenlaag en gaat dus alleen af
voor workspaces waarin identiteitsresolutie over apparaten heen is ingeschakeld.
Een webhook abonneert zich op specifieke types; de aparte waarde * betekent “elk
type, ook types die later worden toegevoegd”. Een onbekend type wordt met 400
geweigerd bij het aanmaken of bewerken van een abonnement, zodat een typefout u niet
kan opzadelen met een webhook die stilzwijgend nooit afgaat.
identification-eventEén bezoek levert tot drie leveringen op die dezelfde requestId delen:
primary — het eerste verdict, bij het laden van de pagina.late — verrijking ongeveer negen seconden later, zodra de trage controles binnen zijn.correction — een correctie op basis van gedrag (aanwijzer, toetsenbord, scrollen).Correleer ze op requestId en onderscheid ze op phase. De latere fase heeft
voorrang: als primary human zei en correction zegt bot, dan is de tweede het
juiste antwoord.
Vertrouw niet op de volgorde van binnenkomst. Elke fase wordt onafhankelijk en met
een eigen retry-schema geleverd — als primary in een retry belandde terwijl late
bij de eerste poging slaagde, ontvangt u ze in omgekeerde volgorde. Bepaal de voorrang
op het veld phase, niet op het tijdstip van ontvangst.
Dat zijn de enige drie fasen van een identification-event. Eén andere waarde bereikt
u wel: account_takeover draagt phase: "beacon", omdat een alarm over
accountovername altijd alleen vanuit een gedragsbeacon wordt opgeworpen.
Let op de discrepantie die dit oplevert, want die raakt de idempotentie. Een
identification-levering in productie heeft als eventId exact
<requestId>:<phase>, maar twee leveringen doorbreken die formule. Een
account_takeover is <requestId>:ato — het achtervoegsel is de letterlijke tekst
ato, niet de waarde van het veld phase. Een testlevering die vanuit het dashboard
wordt verstuurd is <requestId>:test, terwijl phase in de body van schema 2 nog
steeds primary zegt — en een body van schema 1 heeft helemaal geen veld phase, dus
de header is de enige plek waar dat achtervoegsel opduikt. Gebruik eventId
rechtstreeks als idempotentiesleutel en stel hem nooit opnieuw samen uit requestId en
phase. Match op de waarden die u afhandelt en negeer al het andere in plaats van de
levering te weigeren.
attack_detected is een event op workspace-niveau: het heeft geen requestId, geen
visitorId en geen van de blokken browser, geo, bot of decision — die sleutels
ontbreken simpelweg. account_takeover wordt geproduceerd door een specifiek bezoek en
bevat de volledige identificatiebody van uw abonnement plus een accountAlert-blok.
Als u alle events in één handler verwerkt, controleer dan event voordat u
bezoekvelden aanraakt.
| Versie | Voor wie | Hoe over te schakelen |
|---|---|---|
1 | Webhooks die zijn aangemaakt vóór v2 bestond | Blijft voor hen de standaard |
2 | Nieuwe webhooks | De schakelaar op de webhook-kaart in het dashboard |
Schema v1 is bevroren — geen van de velden verandert, zodat bestaande integraties zonder aanpassingen blijven werken. Alles wat nieuw is leeft in v2, en dat is wat nieuwe webhooks uitsturen.
{ "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 van de videoadapter, genormaliseerd; ontbreekt wanneer onbekend "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 // HTTP-verkeer en het rauwe netwerkpad komen via verschillende netwerken naar buiten }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Nul- en lege waarden worden weggelaten. String- en numerieke velden met een
nulwaarde (bot.type bij een mens, bijvoorbeeld) ontbreken in de JSON — maak ze niet
verplicht in uw schema's en lees geneste blokken defensief.
bot.score en decision.riskScore zijn decimalen op een 0..100-schaal met één
cijfer achter de komma — precies de getallen die het dashboard voor hetzelfde bezoek
rapporteert. (In het bevroren v1-schema gebruiken ze andere eenheden: een breuk 0..1
respectievelijk 0..255.)
bot.type is ofwel de naam van een herkende bot, ofwel een familielabel. Zie
Bot-types voor het vocabulaire — interne namen van
controles worden nooit blootgegeven, op geen enkel abonnement.
| Veld | Type | Beschrijving |
|---|---|---|
version | number | Versie van het payload-schema (2) |
event | string | Type event |
eventId | string | Identificatie van de levering — de idempotentiesleutel |
requestId | string | Identificatie van het bezoek (UUID), gedeeld door al zijn fasen |
phase | string | primary, late, correction; account_takeover draagt beacon |
visitorId | string | Stabiele bezoekersidentificatie |
linkedId | string | Gekoppelde identificatie aangeleverd door de client |
tag | string | Aangepaste tag aangeleverd door de client |
timestamp | string | Tijdstip van het event (RFC 3339) |
url | string | URL van de pagina waar het event is vastgelegd |
ip | string | IP-adres van de client |
userAgent | string | Ruwe user-agent-string van de client |
browser.name / .version | string | Gedetecteerde browser |
os.name / .version | string | Gedetecteerd besturingssysteem |
device | string | Apparaatklasse (bijv. desktop, mobile) |
gpu | string | Model van de videoadapter zoals de browser die meldt (WebGL), genormaliseerd naar een leesbare naam (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer bij software-rasterizers; ontbreekt wanneer onbekend |
geo | object | IP-geolokalisatie: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (booleans) en connectionType |
network.proxyDetected | boolean | Het HTTP-verkeer van het bezoek en de rauwe netwerkpaden ervan komen via verschillende netwerken naar buiten — een proxy of VPN vóór de browser. Twee adressen van dezelfde provider (carrier-NAT, een tweede uitgang van hetzelfde VPN) tellen niet mee |
bot.result | string | human, bot of uncertain |
bot.type | string | Botnaam of familielabel wanneer een bot wordt gedetecteerd |
bot.score | number | Bot-score (0–100) |
identification.confidence | number | Confidence van de identificatie (0.0–1.0) |
identification.incognito | boolean | Privé-/incognito-browsingcontext |
decision.action | string | real, fake of suspicious |
decision.riskScore | number | Geaggregeerde risicoscore (0–100) |
Vanaf Pro — hoe de bezoeker zich in de tijd gedraagt:
{ "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}Vanaf Business — waarom het verdict is uitgevallen zoals het uitviel:
{ "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, // het publieke adres dat op het rauwe netwerkpad is waargenomen, dat wil // zeggen het adres achter de proxy of VPN; ontbreekt wanneer zo'n adres // niet is waargenomen "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // De desktopomgeving die GEMETEN is op een Linux-machine ("Mint 22+", // "Ubuntu", "GNOME", "KDE"); een User-Agent kan geen distributie // uitdrukken. Ontbreekt wanneer die niet is vastgesteld — bij de meeste // Linux-bezoeken en bij elk niet-Linux-bezoek. "osEnvironment": "Mint 22+", // Wat het bezoek over zichzelf beweerde, tegenover wat onafhankelijke // controles hebben gemeten. Alleen aanwezig wanneer er daadwerkelijk een // spoof is gedetecteerd; een leeg veld `real` betekent "de controle bleef // stil", nooit "bevestigd". Bij de as `gpu` draagt `claimed.gpu` de // beweerde adapter als dezelfde leesbare modelnaam als het veld `gpu` op // het hoogste niveau. "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"] } }, // Apparaatfeiten — wat de browser van de bezoeker over de machine meldt, // aan onze kant opgeschoond. screen: resolutie, kleurdiepte en de device // pixel ratio. locale: de eigen voorkeurstalen en tijdzone van de browser // — in tegenstelling tot geo.timezone, dat uit het IP-adres wordt // afgeleid; een verschil tussen die twee is een veelvoorkomend teken van // een vervalste locatie. clientHints: User-Agent Client Hints — // CPU-architectuur en bitness, apparaatmodel (Android: de modelcode in // `model`, bijv. "SM-A556B", en de marketingnaam ervan uit de // Google Play-apparaatlijst in `deviceName`, bijv. // "Samsung Galaxy A55 5G") en de exacte platformversie; alleen // Chromium-gebaseerde browsers melden ze. Elk blok ontbreekt wanneer het // bezoek geen dergelijke gegevens meebracht, dus behandel elk als // optioneel. "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" }, // Alleen aanwezig wanneer de videoadapter zichzelf als virtueel // bekendmaakte; hypervisor is een gesloten woordenboek (vmware, // virtualbox, parallels, qemu, hyperv, bochs, intel-gvt, vgpu). Een // ontbrekend blok betekent dat er geen dergelijke aanwijzing is. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Zie Botdetectie voor het vocabulaire van
de reason codes en wat severity betekent.
guidance bevat kant-en-klare “wat te doen”-aanbevelingen per integratiepunt, zodat u
geen beleid hoeft af te leiden uit ruwe scores:
{ "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 }}Elk scenario begint bij allow en beweegt alleen omhoog op de ladder:
allow → challenge → review → deny. Binnen een scenario wint de strengste as die
afgaat, en overall is de strengste over alle vier scenario's.
| Advies | Betaling | Registratie | Login | Affiliate |
|---|---|---|---|---|
allow | Verwerk hem | Maak het aan | Laat hem binnen | Schrijf de conversie bij |
challenge | 3-D Secure / bevestiging | Captcha, bevestiging per e-mail of telefoon | Step-up-2FA, opnieuw authenticeren | Markeer als twijfelachtig tot er activiteit is |
review | Verwerk, maar zet in de wachtrij voor beoordeling | Maak aan met beperkingen | Laat binnen en sla alarm | Houd de uitbetaling vast tot na beoordeling |
deny | Verwerk de transactie niet | Weiger het account aan te maken | Laat hem niet binnen | Schrijf de conversie niet bij |
version is de versie van de regelset — die wordt opgehoogd naarmate de logica
verbetert. Guidance is additief: nieuwe scenario's komen als nieuwe sleutels binnen
zonder het contract te breken. De latere fase wint, behalve bij gedeeltelijk advies:
een levering die op een onvolledige set invoer is berekend, is gemarkeerd met
"partial": true, en gedeeltelijk advies overschrijft geen volledig advies dat
eerder voor dezelfde requestId is ontvangen. In een gewone levering ontbreekt het veld
partial volledig.
Exacte drempels worden bewust niet gedocumenteerd. Advies dat via reverse engineering tot een score te herleiden is, houdt op een verdediging te zijn.
account_takeover-eventAlleen Business en Enterprise. De body is de volledige identificatie-envelop van uw
abonnement plus een accountAlert-blok, hoogstens één keer per bezoek geleverd:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // het alarm komt van een beacon; alleen de eventId zegt "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}In v1 bevat dit blok type, linkedId en drift; in v2 worden twee velden hernoemd —
linkedId → accountId en drift → driftScore. Werk uw handler bij wanneer u
payloadVersion omzet, anders ziet uw accountovername-logica de gegevens stilzwijgend
niet meer.
attack_detected-event{ "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 }}Elke levering bevat een X-Tracio-Signature-header:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t is de Unix-tijdstempel (in seconden) waarop het verzoek is ondertekend.v1 is de hex-gecodeerde HMAC-SHA256 van "<t>.<rawRequestBody>", met uw
webhook-secret als sleutel.De tijdstempel maakt deel uit van de ondertekende inhoud, wat replay-bescherming geeft.
Twee dingen moeten kloppen, anders faalt de verificatie in productie:
v1=-waarde. Tijdens een secret-rotatie bevat de
header twee handtekeningen, en een parser die er maar één van bewaart, weigert
geldige leveringen gedurende het hele rotatievenster.// 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")})Leveringen volgens schema 2 bevatten daarnaast X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Beide kanten kennen het HMAC-secret, dus de HMAC
bewijst dat de afzender het secret kent, maar niet dat het verzoek van TRACIO afkomstig
is; de asymmetrische handtekening doet dat wel. De publieke sleutels worden
gepubliceerd op https://api.tracio.ai/.well-known/webhook-keys, geïndexeerd op kid.
Testleveringen die vanuit het dashboard worden verstuurd, zijn alleen met HMAC
ondertekend — de private platformsleutel staat op de leveringsnodes en is bewust niet
beschikbaar voor het dashboard. Een verificator die Ed25519 hard vereist, moet
testleveringen doorlaten (ze dragen het achtervoegsel :test op eventId), anders
mislukt testen vanuit het dashboard terwijl productie gezond is. Dezelfde
voorzichtigheid geldt voor formaatcontroles: een testlevering draagt een requestId in
de vorm test_<hex> en de letterlijke tekst test_visitor als visitorId, dus een
handler die die tegen de productievormen valideert, weigert een levering die verder
prima in orde is.
Na een rotatie blijven beide secrets 24 uur geldig en bevat de header beide handtekeningen, zodat u uw configuratie kunt bijwerken zonder leveringen te verliezen. De actie Revoke now kort het venster in. Werk het secret aan uw kant binnen 24 uur bij: zodra het venster sluit, komt het oude secret niet meer overeen, en als uw endpoint een ongeldige handtekening met een 4xx beantwoordt, schakelen vijf van zulke antwoorden op rij de webhook uit.
| Header | Beschrijving |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — twee v1= tijdens een rotatievenster |
X-Tracio-Signature-Ed25519 | Platformhandtekening, t=<unix>,kid=<id>,v1=<base64> (alleen v2) |
X-Tracio-Event-Id | Identificatie van de levering — de idempotentiesleutel |
X-Tracio-Request-Id | Identificatie van het bezoek (v2, alleen bezoek-events) |
X-Tracio-Event-Type | Het type event (alleen v2) |
X-Tracio-Delivery-Attempt | Pogingnummer, beginnend bij 1 (alleen v2) |
X-Tracio-Payload-Version | 2 (alleen v2) |
X-Tracio-Webhook-Id | Identificatie van de webhook die deze levering heeft geproduceerd |
Leveringen kunnen opnieuw worden geprobeerd, en een retry draagt dezelfde
X-Tracio-Event-Id. Dedupliceer daarop:
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")})Let op: eventId is uniek per event, niet per webhook: als meerdere webhooks in de
workspace op hetzelfde event zijn geabonneerd, ontvangt elk een levering met dezelfde
identificatie. Hij wordt opgebouwd als <requestId>:<phase>, en daarom worden de drie
fasen van één bezoek onafhankelijk van elkaar gededupliceerd in plaats van samen te
vallen tot één.
Antwoord met 2xx — dat is het enige teken dat een levering is geaccepteerd.
| Antwoord | Wat er gebeurt |
|---|---|
2xx | Levering voltooid |
429 Too Many Requests | Telt niet als mislukking en kost geen poging; een langere Retry-After wordt gerespecteerd |
408, 425, 5xx, verbroken verbinding | Opnieuw geprobeerd met een groeiende pauze |
410 Gone | Het endpoint wordt als verwijderd beschouwd — de webhook wordt onmiddellijk uitgeschakeld |
Overige 4xx | Opnieuw geprobeerd, maar vijf op rij schakelen de webhook uit — 400/401/404 genezen niet door opnieuw te proberen |
Retry-schema: 5 s → 30 s → 2 min → 10 min → 30 min → 2 u → 6 u (8 pogingen). De eerste retries passen binnen een minuut, dus een korte herstart van uw dienst kost u geen melding. Elke pauze wordt gerandomiseerd tussen de helft en de volle waarde, zodat retries na een storing niet in één salvo afgaan.
Automatisch uitschakelen vereist zowel een drempel (20 opeenvolgende mislukkingen, of 5 configuratiefouten) als minstens 15 aaneengesloten minuten met mislukkingen — een korte herstart kan de integratie niet om zeep helpen, zelfs niet als er veel leveringen in de wachtrij stonden. Een gat van meer dan 15 minuten start de telling opnieuw. Het dashboard toont de reden, met de responscode en de fouttekst, en een knop Re-enable die de tellers terugzet.
| Abonnement | Webhooks per workspace |
|---|---|
| Free | Niet beschikbaar |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoints moeten https zijn met een publiek IP-adres — privé- en loopback-adressen
worden geweigerd, ook bij een redirect — en niet meer dan twee redirects diep.
Alleen 307- en 308-redirects worden gevolgd. 301, 302 en 303 dragen de
client op over te schakelen naar GET en de body weg te gooien, dus een levering volgt
ze niet en de poging telt als mislukt. Als uw load balancer de URL normaliseert (door
www of een afsluitende slash toe te voegen), richt de webhook dan direct op de
uiteindelijke URL.
Webhooks worden beheerd in het dashboard. Het dashboard stuurt een workspace-scoped
management-API aan, aangeboden op de applicatiehost (bijvoorbeeld
https://app.tracio.ai/api/v1), en de endpoints hieronder zijn precies wat het
aanroept. Elk webhook-endpoint staat onder /workspaces/{wsId}.
Dit is geen server-naar-server-oppervlak. De management-API accepteert alleen de JWT van uw dashboard-sessie, gecontroleerd tegen uw workspace-rol (RBAC); een geheime sleutel
tracio_sk_…wordt hier geweigerd. Omdat die sessie in de browser leeft en daarmee verloopt, kunt u de onderstaande aanroepen beter opvatten als een beschrijving van wat het dashboard doet dan als een integratie om te automatiseren. Gebruik voor programmatische toegang vanuit uw eigen backend de alleen-lezen Data API.
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": [] }'Het ondertekeningssecret wordt door TRACIO gegenereerd en één keer teruggegeven bij
het aanmaken (en bij het roteren) onder signingSecret. Bewaar het veilig — het is de
sleutel waarmee u handtekeningen verifieert.
{ "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" }}Bij latere leesacties is het signingSecret gemaskeerd (null) — het wordt alleen
onthuld bij het aanmaken en bij het roteren van het secret.
| Methode | Pad | Beschrijving |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Webhooks opsommen |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | url / events / status bijwerken |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Een webhook verwijderen |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Een ondertekende testlevering sturen |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Het ondertekeningssecret roteren |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Recente leveringspogingen opsommen |
Geef zo snel mogelijk een 2xx terug en verwerk de payload asynchroon om timeouts te
voorkomen:
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) }}Gebruik de actie Test op een webhook (of POST .../webhooks/{webhookId}/test) om
een ondertekende voorbeeld-payload naar uw endpoint te sturen en te bevestigen dat het
bereikbaar is en handtekeningen correct verifieert.
Voor lokale ontwikkeling stelt u uw server beschikbaar via een tunnel zoals ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint