Webhooks leveren identificatie-events in realtime aan je server. Elke keer dat een
bezoeker wordt geïdentificeerd, stuurt TRACIO een HTTP-POST-verzoek naar de
webhook-URL die je 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+ |
account_assessment | De beoordelingsstatus van een account voor accountovername: beoordeeld en in orde, beoordeeld en afwijkend, of niet beoordeeld | Business+ |
attack_detected | Een piek aan bots op je site | Business+ |
reputation_changed | De reputatie van een gekoppelde persoon is naar een andere band verschoven | Business+ |
transaction_state_changed | De uitkomst van een transactie die je naar POST /v2/transactions hebt gestuurd, is gewijzigd | Business+ |
login_risk | Een loginbeoordeling via POST /v2/logins/assess eindigde met challenge of review | Alle |
trusted_device_enrolled | Een apparaat is voor een account als vertrouwd ingesteld, of het vertrouwen ervan is verlengd | Alle |
trusted_device_revoked | Het vertrouwen van een apparaat is ingetrokken — via de API of vanuit het dashboard | Alle |
affiliate_threshold_crossed | De aanbeveling voor een affiliate is verslechterd (none → review, none → hold, review → hold) | Alle |
Event-namen gebruiken underscores, nooit punten — er bestaat geen visitor.created of
session.created.
Een webhook abonneert zich op specifieke types; de aparte waarde * betekent “elk
type, ook types die later worden toegevoegd”. Vijf types vormen de uitzondering:
* omvat account_assessment, login_risk, trusted_device_enrolled,
trusted_device_revoked en affiliate_threshold_crossed niet — noem elk ervan
expliciet. account_assessment meldt dekking in plaats van een bevinding en komt
daardoor ongeveer één keer binnen per bezoek dat een linked_id draagt — een heel
andere orde van volume dan de overige events. De types voor loginrisico en affiliates
hebben alleen zin voor een integratie die die API's aanroept. Een wildcard die je hebt
ingesteld voordat deze events bestonden, is geen toestemming om ze te ontvangen.
Een onbekend type wordt met 400 geweigerd bij het aanmaken of bewerken van een
abonnement, zodat een typefout je niet kan opzadelen met een webhook die stilzwijgend
nooit afgaat.
identification-eventEén bezoek levert tot drie leveringen op die dezelfde request_id 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 request_id 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, ontvang je 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
je wel: account_takeover en account_assessment dragen phase: "beacon", omdat
beide vanuit de gedragsbeacon van de sessie worden opgeworpen.
Let op de discrepantie die dit oplevert, want die raakt de idempotentie. Een
identification-levering in productie heeft als event_id exact
<request_id>:<phase>, maar andere leveringen doorbreken die formule. Een
account_takeover is <request_id>:ato — het achtervoegsel is de letterlijke tekst
ato, niet de waarde van het veld phase — en een account_assessment is
<request_id>:ato-state-<state>. Een testlevering die vanuit het dashboard wordt
verstuurd is <request_id>:test, terwijl phase in de body 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 event_id rechtstreeks als
idempotentiesleutel en stel hem nooit opnieuw samen uit request_id en phase. Match
op de waarden die je afhandelt en negeer al het andere in plaats van de levering te
weigeren.
Sommige events hebben geen bezoek: attack_detected (workspace-niveau),
reputation_changed (persoonsniveau), transaction_state_changed
(transactieniveau), login_risk, trusted_device_enrolled en
trusted_device_revoked (accountniveau) en affiliate_threshold_crossed
(affiliateniveau). Ze hebben geen request_id, geen visitor_id en geen van de
blokken browser, geo, bot of decision — die sleutels ontbreken simpelweg, net
als de header X-Tracio-Request-Id. account_takeover en account_assessment worden
daarentegen door een specifiek bezoek geproduceerd en bevatten de volledige
identificatiebody van je abonnement. Als je alle events in één handler verwerkt,
controleer dan event voordat je bezoekvelden aanraakt.
Nieuwe webhooks gebruiken payloadversie 3. Bestaande webhooks op versie 1 of 2 behouden de versie die ze hebben gekozen; beide oudere schema's zijn bevroren en worden op 2027-04-12 verwijderd. Om te migreren selecteer je versie 3 op de webhook-kaart in het dashboard.
| Versie | Status |
|---|---|
1 | Bevroren; verwijderd op 2027-04-12 |
2 | Bevroren; verwijderd op 2027-04-12 |
3 | De standaard voor nieuwe webhooks; snake_case-sleutels |
Versie 3 bevat dezelfde velden en waarden als versie 2, met snake_case-sleutels en een
version: 3 op het hoogste niveau. Het handtekeningalgoritme en de headers zijn
ongewijzigd (X-Tracio-Payload-Version: 3). Sleutels binnen je eigen
fields-woordenboek zijn jouw gegevens en worden niet hernoemd.
| v2 | v3 |
|---|---|
eventId | event_id |
requestId | request_id |
visitorId | visitor_id |
linkedId | linked_id |
userAgent | user_agent |
network.connectionType | network.connection_type |
network.proxyDetected | network.proxy_detected |
network.realIp | network.real_ip |
bot.antidetectScore | bot.antidetect_score |
identification.matchType | identification.match_type |
identification.matchConfidence | identification.match_confidence |
identification.incognitoVisits | identification.incognito_visits |
identification.driftScore | identification.drift_score |
decision.riskScore | decision.risk_score |
decision.suspectScore | decision.suspect_score |
deviceInfo | device_info |
deviceInfo.deviceId | device_info.device_id |
deviceInfo.crossBrowser | device_info.cross_browser |
deviceInfo.linkedBrowsers | device_info.linked_browsers |
screen.colorDepth | screen.color_depth |
screen.pixelRatio | screen.pixel_ratio |
clientHints | client_hints |
clientHints.platformVersion | client_hints.platform_version |
clientHints.deviceName | client_hints.device_name |
osEnvironment | os_environment |
environment.virtualMachine | environment.virtual_machine |
spoofing.spoofedAxes | spoofing.spoofed_axes |
spoofing.anonymousBrowser | spoofing.anonymous_browser |
session.durationSeconds | session.duration_seconds |
velocity.uniqueIps | velocity.unique_ips |
velocity.uniqueLocations | velocity.unique_locations |
accountAlert | account_alert |
accountAlert.accountId | account_alert.account_id |
accountAlert.driftScore | account_alert.drift_score |
accountAssessment | account_assessment |
accountAssessment.accountId | account_assessment.account_id |
accountAssessment.profile.windowsEffective | account_assessment.profile.windows_effective |
accountAssessment.profile.oldestWindowAgeSeconds | account_assessment.profile.oldest_window_age_seconds |
attack.windowMinutes | attack.window_minutes |
attack.recentBots | attack.recent_bots |
attack.recentTotal | attack.recent_total |
personId | person_id |
reputation.overallRisk | reputation.overall_risk |
linkedAccountsCount | linked_accounts_count |
linkedVisitorsCount | linked_visitors_count |
reputationChange | reputation_change |
reputationChange.personId | reputation_change.person_id |
reputationChange.previousBand | reputation_change.previous_band |
reputationChange.visitorIds | reputation_change.visitor_ids |
reputationChange.linkedAccountsCount | reputation_change.linked_accounts_count |
{ "version": 3, "event": "identification", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:primary", // "<request_id>:<phase>" — the idempotency key "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", // visit identifier, shared by all phases "phase": "primary", "visitor_id": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "linked_id": "user-42", // your ?lid=, if you passed one "tag": "checkout", "fields": { "order_ref": "A-1029", "plan": "pro" }, // custom fields that arrived with the visit "timestamp": "2026-07-30T12:00:00Z", "url": "https://shop.example.com/checkout", "ip": "203.0.113.44", "user_agent": "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, "connection_type": "DCH", "proxy_detected": 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", "risk_score": 12.2 }, // real | fake | suspicious "extensions": ["metamask", "privacy_badger"] // detected browser extensions; absent when none}Nul- en lege waarden worden weggelaten. String- en numerieke velden met een
nulwaarde (bijvoorbeeld bot.type bij een mens, of bot.antidetect_score: 0)
ontbreken in de JSON — maak ze niet verplicht in je schema's en lees geneste blokken
defensief.
bot.score en decision.risk_score zijn decimalen op een 0..100-schaal met één
cijfer achter de komma — precies de getallen die het dashboard en de Data API voor
hetzelfde bezoek rapporteren. (In het bevroren v1-schema gebruiken ze andere
eenheden: een breuk 0..1 respectievelijk 0..255.)
bot.type is de naam van een herkende bot, een geverifieerde AI-agent
(ai_agent:<vendor>) of een familielabel. Zie Bot-types
voor het vocabulaire — interne namen van controles worden nooit blootgegeven, op geen
enkel abonnement. ai_agent:<vendor> betekent dat het verzoek heeft bewezen dat het
van de infrastructuur van die leverancier kwam — een geldige Web Bot Auth-handtekening
die is gemaakt met een sleutel die de leverancier publiceert, of een adres binnen de
IP-bereiken die de leverancier publiceert — in plaats van dat alleen in de user agent
te beweren. Een verzoek dat beweert een AI-agent te zijn en door elk bewijs dat de
leverancier publiceert wordt tegengesproken, wordt gerapporteerd als impersonation.
| Veld | Type | Beschrijving |
|---|---|---|
version | number | Versie van het payload-schema (3) |
event | string | Type event |
event_id | string | Identificatie van de levering — de idempotentiesleutel; uniek per event en gedeeld door al je webhooks die het ontvangen, stabiel over retries heen |
request_id | string | Identificatie van het bezoek (UUID), gedeeld door al zijn fasen |
phase | string | primary, late, correction; account_takeover en account_assessment dragen beacon |
visitor_id | string | Stabiele bezoekersidentificatie |
linked_id | string | Gekoppelde identificatie aangeleverd door de client |
tag | string | Aangepaste tag aangeleverd door de client |
fields | object | Aangepaste velden die bij het verzamelen aan het bezoek zijn gekoppeld. Alleen wat met het bezoek is meegekomen — een veld dat later via de Data API is gekoppeld, verschijnt nooit in een webhook, in geen enkele fase of retry. Sleutels en waarden zijn strings. Ontbreekt wanneer het bezoek geen velden meebracht |
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 |
user_agent | 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 connection_type |
network.proxy_detected | 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, geverifieerde AI-agent 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.risk_score | number | Geaggregeerde risicoscore (0–100) |
extensions | array | Browserextensies die in dit bezoek zijn gedetecteerd, als stabiele machine-slugs (namen, categorieën en store-links staan in het dashboard en de Data API). Ontbreekt wanneer er niets is gedetecteerd |
Vanaf Pro — hoe de bezoeker zich in de tijd gedraagt:
{ "identification": { "match_type": "exact", // exact | fuzzy | new — how the visitor was recognized "match_confidence": 0.93, "visits": 42, "incognito_visits": 3 }, // Present when visitor counters are available at event time (usually primary). // A missing block means "no data", not "zeros". "velocity": { "events5m": 7, "unique_ips": 2, "unique_locations": 1 }, "bot": { "antidetect_score": 0 }, // antidetect indicators, 0..100 "session": { "duration_seconds": 95 } // the correction phase and the account_takeover alert}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 }, // The smoothed behavior score the account-takeover verdict was made on. It is // measured by beacons that arrive after the identification event, so it is // empty in identification deliveries — see the account_takeover event. "identification": { "drift_score": 0.41 }, "device_info": { "device_id": "…", // the physical device across browsers on it "cross_browser": true, "confidence": 0.88, "linked_browsers": 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 "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // 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. "os_environment": "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" }, "spoofed_axes": ["os", "screen"], // os | gpu | screen | network | browser "anonymous_browser": { "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. client_hints: 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 `device_name`, 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, "color_depth": 30, "pixel_ratio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "client_hints": { "architecture": "arm", "bitness": "64", "platform_version": "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": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}Zie Botdetectie voor het vocabulaire van
de reason codes en wat severity betekent.
Multi-accountingvelden (vanaf Business). Zodra de persoonsmodus voor je workspace
is ingeschakeld, bevat de body ook person_id, een reputation-blok (score,
categories, overall_risk), linked_accounts_count en linked_visitors_count —
of de bezoeker gekoppeld is aan andere bezoekers en accounts. De personenlaag draait
momenteel in observatiemodus, en tot die live gaat, ontbreken deze velden in de body.
Maak ze niet verplicht in je schema's.
guidance bevat kant-en-klare “wat te doen”-aanbevelingen per integratiepunt, zodat je
geen beleid hoeft af te leiden uit ruwe scores. Het is aanwezig in elk v3-event vanaf
Business — allow wordt expliciet vermeld:
{ "guidance": { "version": 3, "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 |
basis somt de assen op die het advies hebben bepaald — een as waarvan het
voorstel milder was dan het aangenomen advies, komt niet op de lijst. Lees het per
code in plaats van uit te gaan van een vaste set:
As (basis) | Wat het betekent |
|---|---|
list | De bezoeker kwam overeen met een van je workspace-lijsten in de modus enforce. Een allow-lijst gaat boven elke as: elk scenario wordt allow en basis is exact ["list"] |
rule | Het bezoek kwam overeen met een van je workspace-regels in de modus enforce; een regel verschuift alleen het advies van zijn eigen scenario (een allow-regel verzacht alleen dat ene) en wordt overgeslagen wanneer een lijst al heeft ingegrepen |
bot | De botdetector concludeerde dat dit automatisering is — weigert elk scenario |
risk | De beslissing en de totale risicoscore van het bezoek |
behavior | Gedragsbiometrie zegt met voldoende zekerheid bot — voegt frictie toe in plaats van een weigering |
account_takeover | Gedrag onder dit account komt niet langer overeen met het profiel van de eigenaar — login naar challenge, betaling naar review |
network | Tor, of een VPN die uit een datacenter komt |
affiliate_pattern | Het patroon van zelf gegenereerde verwijzingen — de affiliateconversie wordt als challenge vastgehouden |
fraud_reputation, multiaccounting | De reputatie en accountclustering van de persoon achter de bezoeker; verschijnen zodra de personenlaag live is |
version is de versie van de regelset — die wordt opgehoogd naarmate de logica
verbetert, dus bewaar hem in je logs. Versie 2 introduceerde de as list, de eerste
die advies kan verzachten; versie 3 heeft de logindrempels van account_takeover
verplaatst naar de schaal van de verdictscore voor accountovername, zodat hetzelfde
bezoek deny kan krijgen waar versie 2 challenge gaf, en omgekeerd.
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 request_id 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. Er is precies één
bewuste uitzondering: account_assessment.verdict.threshold, de beslisgrens van het
verdict voor accountovername dat hieronder wordt beschreven.
account_takeover-eventAlleen Business en Enterprise; op lagere abonnementen wordt geen levering aangemaakt,
zelfs niet als een webhook op dit event is geabonneerd. De body is de volledige
identificatie-envelop van je abonnement plus een account_alert-blok en het
account_assessment-blok dat in de volgende sectie wordt beschreven, hoogstens één
keer per bezoek geleverd:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // het alarm komt van een beacon; alleen de event_id zegt "ato" "account_alert": { "type": "behavior-drift/2", // kind of alert and the version of its construction "account_id": "user-42", // your linked_id for the account "drift_score": 0.71 // the value the verdict was made on }, "account_assessment": { "type": "behavior-drift/2", "account_id": "user-42", "state": "assessed_alert", "verdict": { "severity": "high", "score": 0.71, "threshold": 0.3565, "scale": "manhattan-normalized", "basis": "ring", "windows": 9 }, "profile": { "windows": 6, "windows_effective": 5, "oldest_window_age_seconds": 1837 } } // …the remaining identification fields}Waarom type een versie draagt. De constructie achter de score is al eerder
veranderd: de schaal en de basis van het verdict. behavior-drift/2 is de eerste
geversioneerde constructie. Als je je eigen regels op het getal hebt gekalibreerd,
beschouw een wijziging van deze waarde dan als signaal om ze opnieuw te controleren.
account_alert.drift_score en identification.drift_score zijn hetzelfde getal
(sinds 2026-10-03): de afgevlakte gedragsscore waarop het verdict is gebaseerd,
gemeten tegen de ring van eerdere vertrouwde vensters van de accounteigenaar. Vóór die
datum bevatte identification.drift_score een ruwe afstand op een andere schaal —
meng geen waarden van vóór en na die datum. Wanneer een venster niet is beoordeeld,
ontbreekt het veld in plaats van nul te zijn. Gebruik account_assessment.verdict
wanneer je het getal nodig hebt samen met de drempel waartegen het is vergeleken.
In v1 is het blok accountAlert met type, linkedId en drift, en is er geen
beoordelingsblok. In v2 hernoemt het alarm twee velden — linkedId → accountId en
drift → driftScore — en voegt het accountAssessment toe; v3 gebruikt de
snake_case-namen hierboven. Werk je handler bij wanneer je van versie wisselt, anders
ziet je accountovername-logica de gegevens stilzwijgend niet meer.
account_assessment-eventAlleen Business en Enterprise. Vereist een expliciet abonnement: * omvat dit event
niet.
Zonder dit event betekent het ontbreken van een alarm twee verschillende dingen tegelijk: óf we hebben de sessie beoordeeld tegen het profiel van de accounteigenaar en die kwam overeen, óf we konden haar helemaal niet beoordelen — het profiel van de eigenaar is nog niet klaar, het venster was te schaars of het verkeer kwam niet van een persoon. Beide zien eruit als stilte, en stilte leest als veiligheid. Meestal is dat niet zo: op een volledige productiedag in september 2026 onthielden we ons van een verdict bij 73% van de vensters en 80% van de accounts. Een proces dat is gebouwd op “geen alarm betekent in orde”, is gebouwd op een toestand die we meestal niet hebben.
{ "version": 3, "event": "account_assessment", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato-state-not_assessed", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", "account_assessment": { "type": "behavior-drift/2", "account_id": "user-42", "state": "not_assessed", // always present, one of three values "reason": "profile_not_ready", // only when state is not_assessed "profile": { "windows": 3, // windows in the owner's reference ring "windows_effective": 2, // of those, how many yielded a distance "oldest_window_age_seconds": 1837 } } // …the remaining identification fields}state ontbreekt nooit en is nooit leeg:
state | Betekenis | Wat te doen |
|---|---|---|
assessed_clear | Beoordeeld; gedrag komt overeen met de eigenaar | Niets |
assessed_alert | Beoordeeld; gedrag wijkt af | Step-up — het account_takeover-alarm draagt hetzelfde verdict |
not_assessed | Er is geen verdict gegeven | Behandel dit niet als schoon; val terug op je eigen signalen |
reason (alleen bij not_assessed):
reason | Betekenis |
|---|---|
profile_not_ready | De referentie van de eigenaar kan op dit moment geen verdict voor dit account opleveren. Behandel het als een toestand, niet als een fase: het kan ook nooit veranderen, dus wacht er niet op |
profile_learning | De referentie wordt nog opgebouwd |
window_too_sparse | Er was een persoon aanwezig, maar het venster bevatte te weinig interactie |
not_computable | Het venster en de referentie hadden geen informatieve assen gemeen |
bot_traffic | Er waren genoeg gegevens, en er was geen persoon om te beoordelen |
Sinds 2026-10-04 18:00 UTC worden window_too_sparse en not_computable niet meer
uitgegeven voor een eigenaar wiens referentie klaar is — zo'n venster wordt gemeld als
profile_not_ready. De twee redenen blijven in de lijst staan omdat eerdere
leveringen ze bevatten: als jouw handler erop vertakt, behandel profile_not_ready dan
op dezelfde manier, en vergelijk de frequenties van deze redenen niet over die datum
heen.
state en reason zijn niet aan een abonnement gebonden. verdict is Business+, net
als drift_score.
Het verdict-blok is alleen aanwezig bij assessed_clear en assessed_alert.
Bij not_assessed ontbreekt het blok in plaats van met nullen te zijn gevuld — een
score van 0 naast een threshold van 0 zou lezen als “gedrag kwam exact overeen”.
| Veld | Betekenis |
|---|---|
severity | low of high — bewust twee banden: een middenband zou een tweede drempel op dezelfde schaal vereisen |
score | De waarde waarop het verdict is gebaseerd |
threshold | De waarde waartegen het daadwerkelijk is vergeleken |
scale | Hoe de score is opgebouwd: manhattan-normalized is d/(1+d) in een genormaliseerde gedragsruimte, waarbij 0 een exacte match betekent |
basis | ring — afstand tot het dichtstbijzijnde van de eerdere vertrouwde vensters van de eigenaar; centroid — afstand tot diens gemiddelde profiel |
windows | Hoeveel vensters in de score zijn samengevoegd |
score en threshold staan altijd op dezelfde, benoemde schaal, zodat het paar de
beslissing reproduceert: het alarm gaat precies af wanneer score > threshold.
Hoe vaak het binnenkomt. Hoogstens één levering per toestand per bezoek — in de
praktijk één per bezoek dat een linked_id draagt, niet één per gedragsvenster. Een
toestand kan binnen één bezoek veranderen — bijvoorbeeld wanneer de referentie klaar
is met leren en not_assessed overgaat in assessed_clear — en dan ontvang je beide,
op volgorde. Die overgang is mogelijk, niet beloofd: profile_not_ready kan het hele
bestaan van het account aanhouden.
Wat het niet belooft. Het best beschermd zijn de accounts van actieve gebruikers met lange sessies en een volledig referentieprofiel; het slechtst beschermd zijn onregelmatige gebruikers, voor wie een overname gevaarlijker is. Dit is een eigenschap van de constructie, geen drempel die kan worden bijgesteld. Het event meldt een gedragsafwijking ten opzichte van een referentie — het is geen claim om accountovername in het algemeen te vangen.
attack_detected-eventHet bot-piekalarm komt binnen in de gemeenschappelijke v3-envelop met een
attack-blok. Het heeft geen identificatievelden — dit is een event op
workspace-niveau:
{ "version": 3, "event": "attack_detected", "event_id": "c0a8e1f2-…", // alert id; matches X-Tracio-Event-Id "timestamp": "2026-07-30T12:00:00Z", "attack": { "kind": "bot_spike", "severity": "critical", // warning | critical "window_minutes": 15, "recent_bots": 4210, "recent_total": 5100, "expected": 180.5 // the baseline expected over a window this size }}Webhooks op payloadversie 1 ontvangen het historische platte alarmschema — dat verandert niet.
login_risk, trusted_device_enrolled en trusted_device_revokedDeze drie events komen voort uit je eigen aanroepen van de loginrisico-API
(POST /v2/logins/assess) en de bijbehorende vertrouwde apparaten. Ze worden alleen
geleverd bij een expliciet abonnement op naam — * omvat ze niet — en alleen aan
webhooks op payloadversie 2 of 3; versie 1 heeft geen body voor deze events.
login_risk wordt verstuurd wanneer een beoordeling challenge of review
oplevert — nooit bij allow, deny of not_assessed. Het login_risk-blok is de
opgeslagen beoordeling, precies zoals het synchrone antwoord die gaf. Event-id:
login-risk:<id>, waarbij <id> onze identificatie van het opgeslagen record is —
neem je eigen assessment_id uit de body.trusted_device_enrolled en trusted_device_revoked worden verstuurd bij elke
registratie, verlenging en intrekking van een vertrouwd apparaat. Hun
trusted_device-blok — user_id, device_ref, enrolled_at, expires_at,
revoked_at, revoked_by (api of dashboard) — is een momentopname op het
moment van het event; bij trusted_device_enrolled zijn revoked_at en
revoked_by null. Event-id: trusted-device:<n>.Ze worden asynchroon verstuurd, tot ongeveer vijf seconden na de aanroep, en dragen
geen header X-Tracio-Request-Id — gebruik login_risk.request_id om het bezoek te
vinden. De webhook is een melding voor audit en alerting; de beslissing zelf is het
synchrone antwoord.
{ "version": 3, "event": "login_risk", "event_id": "login-risk:4b1f9e2c-6d3a-4f80-9b7e-2a5c8d1e0f37", "timestamp": "2026-10-17T10:00:00Z", "login_risk": { "assessment_id": "snap-1", "user_id": "alice", "state": "assessed", "recommendation": "challenge", "reasons": ["new_device"] // …the rest of the assessment }}{ "version": 3, "event": "trusted_device_revoked", "event_id": "trusted-device:7", "timestamp": "2026-10-17T11:00:00Z", "trusted_device": { "user_id": "alice", "device_ref": "7978151c54e94838", "enrolled_at": "2026-10-17T10:00:00Z", "expires_at": "2026-11-16T10:00:00Z", "revoked_at": "2026-10-17T11:00:00Z", "revoked_by": "dashboard" }}affiliate_threshold_crossed-eventAlleen geleverd bij een expliciet abonnement op naam — * omvat dit event niet —
en alleen aan webhooks op payloadversie 2 of 3. Elke 15 minuten herberekent TRACIO de
kwaliteitsscore van elke affiliate met voldoende gegevens en verstuurt het event
wanneer de aanbeveling ervan verslechtert: none → review, none → hold of
review → hold. Een verbetering verstuurt niets en zorgt ervoor dat de volgende
verslechtering opnieuw wordt gemeld. Het event hoort bij de workspace: het gaat naar
de geabonneerde webhooks van elke omgeving en draagt geen header
X-Tracio-Request-Id. Event-id:
affiliate-threshold:<affiliate_id>:<recommendation>:<unix time>.
{ "version": 3, "event": "affiliate_threshold_crossed", "event_id": "affiliate-threshold:aff_17:review:1792490400", "timestamp": "2026-10-20T10:00:00Z", "affiliate_threshold": { "affiliate_id": "aff_17", "recommendation": "review", // none | review | hold "previous_recommendation": "none", "quality_score": 72, // 0..100, higher is better "hold_days": 14, // the current hold period of the workspace "crossed_at": "2026-10-20T10:00:00Z", "metrics": { "reg_to_ftd": 0.31, "active14_share": 0.42, "dup_device_share": 0.3, "dup_person_share": 0.1, "ring_share": 0.05, "bonus_hunter_share": 0.2, "dc_vpn_bot_share": 0.1, "chargeback_share": 0.03, "affiliate_fraud_share": 0, "no_visit_share": 0.05 } }}metrics zijn de aandelen van de affiliate over de laatste 90 dagen, dezelfde die het
rapport in het dashboard toont: het aandeel registraties dat een eerste storting
bereikte (reg_to_ftd), het aandeel spelers dat 14 dagen na registratie nog actief is
(active14_share), en de aandelen conversies met een dubbel apparaat of een dubbele
persoon, uit een frauderingnetwerk, van bonusjagers, vanuit een datacenter, VPN of
bot, gelabeld als chargeback of als affiliatefraude, en zonder gevonden bezoek.
TRACIO geeft alleen aanbevelingen — het vasthouden zelf en elke beslissing over een
uitbetaling blijven in je affiliateplatform. Op payloadversie 2 heeft dezelfde body
camelCase-sleutels: het blok is affiliateThreshold, met affiliateId,
previousRecommendation, qualityScore enzovoort.
transaction_state_changed-eventVanaf Business. Wordt verstuurd wanneer de uitkomst verandert van een transactie die je
naar POST /v2/transactions hebt gestuurd: de regels besloten tot REVIEW of
DECLINE (een APPROVE verstuurt niets), een analist heeft de transactie in het
dashboard ter beoordeling doorgestuurd, of een label is ingesteld, vervangen of
verwijderd. Gebruik het om een bonus vast te houden zolang een claim wordt beoordeeld
en die in te houden of vrij te geven zodra de uitkomst vaststaat, zonder te pollen.
* omvat dit event; het wordt alleen geleverd aan webhooks op payloadversie 2 of 3.
Een webhook die aan een omgeving is gekoppeld, ontvangt het alleen voor die omgeving.
{ "version": 3, "event": "transaction_state_changed", "event_id": "txn-state:7d1c2b3a-0000-4000-8000-000000000042:118", "timestamp": "2026-10-15T10:04:00Z", "transaction": { "id": "7d1c2b3a-0000-4000-8000-000000000042", "transaction_id": "bonus-claim-1842", "state": "REVIEW", // always the decision of the rules "fraud_score": 50, "effective_state": "DECLINE", // the outcome to act on "recommendations": ["withhold_bonus"], "label": "fraud_detection_bonus_abuse", "label_source": "api", // api | dashboard "label_at": "2026-10-15T10:04:00Z", "previous_effective_state": "REVIEW" // …applied_rules and evidence, as in the transaction response }}| Sleutel | Betekenis |
|---|---|
effective_state | De uitkomst om op te handelen: de status van de analist of van het label wanneer die is ingesteld, anders state |
recommendations | Wat te doen met de bonus, afgeleid van effective_state |
label, label_source, label_at | Het huidige label, waar het is ingesteld (api of dashboard) en wanneer; alle drie zijn null wanneer de transactie geen label heeft |
previous_effective_state | effective_state vóór deze wijziging; null voor het event dat bij de automatische beslissing wordt verstuurd |
Het blok toont de transactie zoals die is op het moment dat het event wordt
verstuurd: als twee wijzigingen snel op elkaar volgen, dragen beide events de
laatste status, elk met een eigen previous_effective_state en timestamp. De
volgorde is niet gegarandeerd — sorteer op timestamp en lees de transactie via de
API voor de actuele status. Event-id: txn-state:<id>:created voor de automatische
beslissing en txn-state:<id>:<n> voor elke latere wijziging. Het event zegt niet wie
een label heeft ingesteld — alleen waar het vandaan kwam.
reputation_changed-eventVanaf Business. Wordt verstuurd wanneer de klantoverstijgende reputatie van een
persoon die jouw organisatie heeft gezien naar een andere band verschuift (bijvoorbeeld
clean → suspicious). Het vereist de personenlaag, die momenteel in observatiemodus
draait — het event gaat dus alleen af voor workspaces waarin identiteitsresolutie over
apparaten heen is ingeschakeld. Dit is een event op persoonsniveau: het heeft geen
request_id en geen bezoekblokken.
{ "version": 3, "event": "reputation_changed", "event_id": "person-reputation:pn_4f2a…:clean->suspicious:2026-07-30T12:10:00.123456Z", "timestamp": "2026-07-30T12:10:00Z", "reputation_change": { "person_id": "pn_4f2a…", "score": 46, "band": "suspicious", // clean | suspicious | fake "previous_band": "clean", "categories": ["chargeback"], "visitor_ids": ["X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y"], // the person's visitors in YOUR data "linked_accounts_count": 4 // omitted when zero }}De bloksleutel is reputation_change, niet reputation: die laatste is al in gebruik
voor de reputatie in de context van een specifiek bezoek, en dat zijn twee
verschillende dingen. Schema 1 levert de historische platte vorm — dezelfde velden op
het hoogste niveau.
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 je
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.visitor_id}`) console.log(`Bot: ${event.bot?.result}`) // "human" | "bot" | "uncertain"
res.status(200).send("OK")})Leveringen volgens schema 2 en 3 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:
lees het veld public_key. De verouderde alias publicKey bevat dezelfde waarde en
wordt op 2027-04-12 verwijderd.
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 event_id), anders
mislukt testen vanuit het dashboard terwijl productie gezond is. Dezelfde
voorzichtigheid geldt voor formaatcontroles: een testlevering draagt een request_id
in de vorm test_<hex> en de letterlijke tekst test_visitor als visitor_id, 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 je je configuratie kunt bijwerken zonder leveringen te verliezen. De actie Revoke now kort het venster in. Werk het secret aan je kant binnen 24 uur bij: zodra het venster sluit, komt het oude secret niet meer overeen, en als je 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 en v3) |
X-Tracio-Event-Id | Identificatie van de levering — de idempotentiesleutel |
X-Tracio-Request-Id | Identificatie van het bezoek, komt overeen met request_id (alleen v2 en v3). Alleen verstuurd voor bezoek-events — identification, account_takeover en account_assessment |
X-Tracio-Event-Type | Het type event (alleen v2 en v3) |
X-Tracio-Delivery-Attempt | Pogingnummer, beginnend bij 1 (alleen v2 en v3) |
X-Tracio-Payload-Version | 3 of 2 (alleen v2 en v3) |
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: event_id 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. Voor identification wordt hij opgebouwd als
<request_id>:<phase>, en daarom worden de drie fasen van één bezoek onafhankelijk
van elkaar gededupliceerd in plaats van samen te vallen tot één. Events zonder bezoek
hebben hun eigen vormen — zie de afzonderlijke events hierboven.
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 je dienst kost je 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 jouw 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 je dashboard-sessie, gecontroleerd tegen je workspace-rol (RBAC); een geheime sleutel
tracio_sk_…wordt hier geweigerd. Omdat die sessie in de browser leeft en daarmee verloopt, kun je 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 je 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 je 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?.risk_score > 50) { await alertFraudTeam(event) }
if (event.bot?.result === "bot") { await blockVisitor(event.visitor_id) }}Gebruik de actie Test op een webhook (of POST .../webhooks/{webhookId}/test) om
een ondertekende voorbeeld-payload naar je endpoint te sturen en te bevestigen dat het
bereikbaar is en handtekeningen correct verifieert.
Voor lokale ontwikkeling stel je je server beschikbaar via een tunnel zoals ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint