Webhooks levererar identifieringshändelser till din server i realtid. Varje gång en
besökare identifieras skickar TRACIO en HTTP-POST-begäran till den webhook-URL du har
konfigurerat. Begärans body är händelsens payload.
De är dessutom den enda kanal som levererar sena verdikt — de där en besökares beteende visade att den var automatiserad först efter att sidan redan hade laddats.
Du ställer in dem i dashboarden under Settings → Webhooks. Webhooks kräver prisplanen Pro eller högre.
| Händelse | När | Prisplan |
|---|---|---|
identification | Vid varje besök — faserna primary, late och correction | Alla |
account_takeover | Beteendet under ett konto matchar inte längre innehavarens profil | Business+ |
account_assessment | Kontots bedömningsläge för kontokapning: bedömt utan avvikelse, bedömt med avvikelse eller inte bedömt | Business+ |
attack_detected | En topp av bottar på din webbplats | Business+ |
reputation_changed | Ryktet för en länkad person har flyttats till ett annat band | Business+ |
transaction_state_changed | Resultatet för en transaktion som du skickat till POST /v2/transactions har ändrats | Business+ |
login_risk | En inloggningsbedömning via POST /v2/logins/assess slutade med challenge eller review | Alla |
trusted_device_enrolled | En enhet gjordes betrodd för ett konto, eller dess förtroende förnyades | Alla |
trusted_device_revoked | Förtroendet för en enhet återkallades — via API:et eller från dashboarden | Alla |
affiliate_threshold_crossed | Rekommendationen för en affiliate försämrades (none → review, none → hold, review → hold) | Alla |
Händelsenamn använder understreck, aldrig punkter — det finns inget visitor.created
eller session.created.
En webhook prenumererar på specifika typer; det separata värdet * betyder ”varje
typ, även de som läggs till senare”. Fem typer är undantaget: * omfattar inte
account_assessment, login_risk, trusted_device_enrolled,
trusted_device_revoked och affiliate_threshold_crossed — ange var och en av dem
uttryckligen. account_assessment rapporterar täckning snarare än ett fynd, så den
kommer ungefär en gång per besök som bär ett linked_id — en annan storleksordning av
volym än de övriga händelserna. Typerna för inloggningsrisk och affiliates är bara
meningsfulla för en integration som anropar de API:erna. Ett jokertecken som du satte
innan de här händelserna fanns är inget samtycke till att ta emot dem.
En okänd typ avvisas med 400 när en prenumeration skapas eller redigeras, så ett
skrivfel kan inte lämna dig med en webhook som tyst aldrig utlöser.
identificationEtt enda besök ger upp till tre leveranser som delar samma request_id:
primary — det första verdiktet, vid sidladdning.late — berikning ungefär nio sekunder senare, när de långsamma kontrollerna har landat.correction — en korrigering baserad på beteende (pekare, tangentbord, scrollning).Korrelera dem via request_id och skilj dem åt via phase. Den senare fasen har
företräde: om primary sa human och correction säger bot är den andra rätt
svar.
Förlita dig inte på ankomstordningen. Varje fas levereras oberoende och med eget
retry-schema — om primary gick in i retry medan late lyckades på första försöket
får du dem i omvänd ordning. Avgör företrädet utifrån fältet phase, inte utifrån
mottagningstidpunkten.
De tre är de enda faserna i en identification-händelse. Ett annat värde når dig:
account_takeover och account_assessment bär phase: "beacon", eftersom båda
utlöses från sessionens beteendebaserade beacon.
Lägg märke till diskrepansen det skapar, eftersom den påverkar idempotensen. En
identification-leverans i produktion har ett event_id som är exakt
<request_id>:<phase>, men andra leveranser bryter mot den formeln. En
account_takeover är <request_id>:ato — suffixet är den bokstavliga strängen ato,
inte värdet i fältet phase — och en account_assessment är
<request_id>:ato-state-<state>. En testleverans som skickas från dashboarden är
<request_id>:test, medan phase i dess body fortfarande står som primary — och en
body enligt schema 1 har inget phase-fält alls, så headern är det enda stället där
suffixet syns. Använd event_id direkt som idempotensnyckel och sätt aldrig ihop det
på nytt av request_id och phase. Matcha på de värden du hanterar och ignorera allt
annat i stället för att avvisa leveransen.
Vissa händelser har inget besök: attack_detected (workspace-nivå),
reputation_changed (personnivå), transaction_state_changed (transaktionsnivå),
login_risk, trusted_device_enrolled och trusted_device_revoked (kontonivå) samt
affiliate_threshold_crossed (affiliatenivå). De har varken request_id eller
visitor_id, och inget av blocken browser, geo, bot eller decision — de
nycklarna saknas helt enkelt, liksom headern X-Tracio-Request-Id.
account_takeover och account_assessment produceras däremot av ett specifikt besök
och bär hela identifieringskroppen för din prisplan. Om du parsar alla händelser i en
och samma handler, kontrollera event innan du rör besöksfält.
Nya webhooks använder payload-version 3. Befintliga webhooks på version 1 eller 2 behåller den version de valt; båda de äldre schemana är frysta och tas bort den 2027-04-12. För att migrera väljer du version 3 på webhook-kortet i dashboarden.
| Version | Status |
|---|---|
1 | Fryst; tas bort den 2027-04-12 |
2 | Fryst; tas bort den 2027-04-12 |
3 | Standard för nya webhooks; nycklar i snake_case |
Version 3 bär samma fält och värden som version 2, med nycklar i snake_case och ett
version: 3 på översta nivån. Signaturalgoritmen och headers är oförändrade
(X-Tracio-Payload-Version: 3). Nycklarna i din egen ordlista fields är dina data
och döps inte om.
| 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", // grafikkortets modell, normaliserad; saknas när den är okänd "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-trafiken och den råa nätverksvägen går ut genom olika nätverk }, "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}Noll- och tomvärden utelämnas. Sträng- och numeriska fält med värdet noll (till
exempel bot.type för en människa eller bot.antidetect_score: 0) saknas i JSON:en
— gör dem inte obligatoriska i dina scheman och läs nästlade block defensivt.
bot.score och decision.risk_score är decimaltal på skalan 0..100 med en
decimal — exakt de tal som dashboarden och Data-API:et rapporterar för samma besök. (I
det frysta v1-schemat använder de andra enheter: en andel 0..1 respektive 0..255.)
bot.type är antingen namnet på en känd bot, en verifierad AI-agent
(ai_agent:<vendor>) eller en familjeetikett. Se Bottyper
för vokabuläret — interna kontrollnamn exponeras aldrig, i ingen prisplan.
ai_agent:<vendor> betyder att begäran bevisade att den kom från leverantörens
infrastruktur — en giltig Web Bot Auth-signatur gjord med en nyckel som leverantören
publicerar, eller en adress inom de IP-intervall som leverantören publicerar — i
stället för att bara påstå det i user-agent-strängen. En begäran som påstår sig vara en
AI-agent och motsägs av varje bevis som leverantören publicerar rapporteras som
impersonation.
| Fält | Typ | Beskrivning |
|---|---|---|
version | number | Version av payload-schemat (3) |
event | string | Händelsetyp |
event_id | string | Identifierare för leveransen — idempotensnyckeln; unik per händelse och delad av alla dina webhooks som tar emot den, stabil över omförsök |
request_id | string | Identifierare för besöket (UUID), delad av alla dess faser |
phase | string | primary, late, correction; account_takeover och account_assessment bär beacon |
visitor_id | string | Stabil besökaridentifierare |
linked_id | string | Länkad identifierare som klienten skickat med |
tag | string | Egen tagg som klienten skickat med |
fields | object | Egna fält som kopplats till besöket vid insamlingen. Endast det som kom med besöket — ett fält som kopplas senare via Data-API:et syns aldrig i en webhook, i någon fas eller vid något omförsök. Nycklar och värden är strängar. Saknas när besöket inte bar några fält |
timestamp | string | Händelsetid (RFC 3339) |
url | string | URL till sidan där händelsen fångades |
ip | string | Klientens IP-adress |
user_agent | string | Klientens råa user-agent-sträng |
browser.name / .version | string | Identifierad webbläsare |
os.name / .version | string | Identifierat operativsystem |
device | string | Enhetsklass (t.ex. desktop, mobile) |
gpu | string | Grafikkortets modell så som webbläsaren rapporterar den (WebGL), normaliserad till ett läsbart namn (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer för programvarurasterare; saknas när den är okänd |
geo | object | IP-geolokalisering: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (booleaner) och connection_type |
network.proxy_detected | boolean | Besökets HTTP-trafik och dess råa nätverksvägar går ut genom olika nätverk — en proxy eller ett VPN framför webbläsaren. Två adresser hos samma leverantör (carrier-NAT, en andra utgång i samma VPN) räknas inte |
bot.result | string | human, bot eller uncertain |
bot.type | string | Botnamn, verifierad AI-agent eller familjeetikett när en bot upptäcks |
bot.score | number | Botpoäng (0–100) |
identification.confidence | number | Identifieringens konfidens (0.0–1.0) |
identification.incognito | boolean | Privat/inkognito surfkontext |
decision.action | string | real, fake eller suspicious |
decision.risk_score | number | Aggregerad riskpoäng (0–100) |
extensions | array | Webbläsartillägg som upptäckts vid det här besöket, som stabila maskinläsbara slugs (namn, kategorier och butikslänkar finns i dashboarden och Data-API:et). Saknas när inget har upptäckts |
Från Pro och uppåt — hur besökaren beter sig över tid:
{ "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}Från Business och uppåt — varför verdiktet blev som det blev:
{ "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, // den publika adress som observerats på den råa nätverksvägen, det vill // säga adressen bakom proxyn eller VPN:et; saknas när ingen sådan adress // observerats "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // Skrivbordsmiljön som MÄTTS på en Linux-maskin ("Mint 22+", "Ubuntu", // "GNOME", "KDE"); en User-Agent kan inte uttrycka en distribution. Saknas // när den inte har fastställts — vid de flesta Linux-besök och vid varje // besök som inte är Linux. "os_environment": "Mint 22+", // Vad besöket påstod om sig självt jämfört med vad oberoende kontroller // mätte. Finns endast när en förfalskning faktiskt upptäckts; ett tomt // fält `real` betyder "kontrollen förblev tyst", aldrig "bekräftat". På // axeln `gpu` bär `claimed.gpu` det påstådda grafikkortet med samma // läsbara modellnamn som fältet `gpu` på översta nivån. "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"] } }, // Enhetsfakta — vad besökarens webbläsare rapporterar om maskinen, sanerat // på vår sida. screen: upplösning, färgdjup och device pixel ratio. // locale: webbläsarens egna föredragna språk och tidszon — till skillnad // från geo.timezone, som härleds från IP-adressen; en avvikelse mellan de // två är ett vanligt tecken på en förfalskad plats. client_hints: // User-Agent Client Hints — CPU-arkitektur och bitness, enhetsmodell // (Android: modellkoden i `model`, t.ex. "SM-A556B", och dess // marknadsnamn från Google Plays enhetslista i `device_name`, t.ex. // "Samsung Galaxy A55 5G") och den exakta plattformsversionen; endast // Chromium-baserade webbläsare rapporterar dem. Varje block saknas när // besöket inte bar med sig sådana data, så behandla vart och ett som // valfritt. "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" }, // Finns endast när grafikkortet angav sig självt som virtuellt; hypervisor // är en sluten ordlista (vmware, virtualbox, parallels, qemu, hyperv, // bochs, intel-gvt, vgpu). Ett block som saknas betyder att inget sådant // belägg finns. "environment": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}Se Bot-detektering för vokabuläret av
orsakskoder och vad severity betyder.
Fält för flerkontohantering (från Business och uppåt). När personläget är aktiverat
för ditt workspace bär kroppen även person_id, ett reputation-block (score,
categories, overall_risk), linked_accounts_count och linked_visitors_count —
om besökaren är länkad till andra besökare och konton. Personlagret körs för
närvarande i observationsläge, och tills det går live saknas de här fälten i kroppen.
Gör dem inte obligatoriska i dina scheman.
guidance bär färdiga ”vad du bör göra”-rekommendationer per integrationspunkt, så att
du slipper härleda en policy ur råa poäng. Det finns i varje v3-händelse för
Business+ — allow anges uttryckligen:
{ "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 }}Varje scenario startar på allow och kan bara röra sig uppåt på stegen:
allow → challenge → review → deny. Inom ett scenario vinner den strängaste axel
som utlöser, och overall är den strängaste av alla fyra scenarierna.
| Råd | Betalning | Registrering | Inloggning | Affiliate |
|---|---|---|---|---|
allow | Genomför den | Skapa det | Släpp in | Kreditera konverteringen |
challenge | 3-D Secure / bekräftelse | Captcha, bekräftelse via e-post eller telefon | Step-up-2FA, autentisera på nytt | Markera som tveksam tills aktivitet syns |
review | Genomför, men lägg i kö för granskning | Skapa med begränsningar | Släpp in och larma | Håll inne utbetalningen tills den granskats |
deny | Genomför inte transaktionen | Neka att skapa kontot | Släpp inte in | Kreditera inte konverteringen |
basis listar de axlar som avgjorde rådet — en axel vars förslag var mildare än
det som antogs kommer inte med på listan. Läs den per kod i stället för att anta en
fast uppsättning:
Axel (basis) | Vad den betyder |
|---|---|
list | Besökaren matchade en av dina workspace-listor i läget enforce. En allow-lista åsidosätter varje axel: varje scenario blir allow och basis är exakt ["list"] |
rule | Besöket matchade en av dina workspace-regler i läget enforce; en regel flyttar bara rådet för sitt eget scenario (en allow-regel mildrar bara just det), och hoppas över när en lista redan har agerat |
bot | Botdetektorn har kommit fram till att det rör sig om automatisering — nekar varje scenario |
risk | Beslutet och besökets samlade riskpoäng |
behavior | Beteendebiometrin säger bot med tillräcklig konfidens — lägger till friktion snarare än nekande |
account_takeover | Beteendet under det här kontot matchar inte längre innehavarens profil — inloggning till challenge, betalning till review |
network | Tor, eller ett VPN som går ut från ett datacenter |
affiliate_pattern | Mönstret för självgenererade hänvisningar — affiliate-konverteringen hålls som challenge |
fraud_reputation, multiaccounting | Ryktet och kontoklustringen för personen bakom besökaren; dyker upp när personlagret är live |
version är versionen av regeluppsättningen — den räknas upp allteftersom logiken
förbättras, så behåll den i dina loggar. Version 2 införde axeln list, den första som
kan mildra ett råd; version 3 flyttade inloggningströsklarna för account_takeover till
skalan för kontokapningsverdiktets poäng, så samma besök kan få deny där version 2 gav
challenge, och tvärtom.
Guidance är additivt: nya scenarier tillkommer som nya nycklar utan att bryta
kontraktet. Den senare fasen vinner, utom för partiella råd: en leverans som
beräknats på en ofullständig uppsättning indata är märkt med "partial": true, och ett
partiellt råd åsidosätter inte ett fullständigt råd som tagits emot tidigare för
samma request_id. I en vanlig leverans saknas fältet partial helt.
Exakta trösklar dokumenteras avsiktligt inte. Ett råd som kan bakåtkompileras till en
poäng upphör att vara ett skydd. Det finns exakt ett avsiktligt undantag:
account_assessment.verdict.threshold, beslutsgränsen för kontokapningsverdiktet som
beskrivs nedan.
account_takeoverEndast Business och Enterprise; på lägre prisplaner skapas ingen leverans, även om en
webhook prenumererar på den här händelsen. Kroppen är hela identifieringskuvertet för
din prisplan plus ett account_alert-block och det account_assessment-block som
beskrivs i nästa avsnitt, levererat högst en gång per besök:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // larmet utlöses från en beacon; bara event_id säger "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}Varför type bär en version. Konstruktionen bakom poängen har ändrats tidigare:
skalan och verdiktets grund. behavior-drift/2 är den första versionerade
konstruktionen. Om du har kalibrerat dina egna regler på talet, behandla en ändring av
det här värdet som en signal att kontrollera dem på nytt.
account_alert.drift_score och identification.drift_score är samma tal (sedan
2026-10-03): den utjämnade beteendepoäng som verdiktet fattades på, mätt mot
kontoinnehavarens ring av tidigare betrodda fönster. Före det datumet bar
identification.drift_score ett rått avstånd på en annan skala — blanda inte värden
från före och efter det datumet. När ett fönster inte bedömdes saknas fältet i stället
för att vara noll. Använd account_assessment.verdict när du behöver talet tillsammans
med tröskeln det jämfördes mot.
I v1 heter blocket accountAlert med type, linkedId och drift, och det finns
inget bedömningsblock. I v2 byter larmet namn på två fält — linkedId → accountId
och drift → driftScore — och lägger till accountAssessment; v3 använder
snake_case-namnen ovan. Uppdatera din handler när du byter version, annars slutar din
logik för kontokapning tyst att se datan.
account_assessmentEndast Business och Enterprise. Kräver en uttrycklig prenumeration: * omfattar den
inte.
Utan den här händelsen betyder frånvaron av ett larm två olika saker på en gång: antingen bedömde vi sessionen mot kontoinnehavarens profil och den matchade, eller så kunde vi inte bedöma den alls — innehavarens profil är inte redo, fönstret var för glest eller trafiken kom inte från en person. Båda ser ut som tystnad, och tystnad läses som säkerhet. Det är den oftast inte: under en hel produktionsdag i september 2026 avstod vi från ett verdikt för 73 % av fönstren och 80 % av kontona. En process som bygger på ”inget larm betyder att det är lugnt” bygger på ett tillstånd som vi för det mesta inte har.
{ "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 saknas aldrig och är aldrig tomt:
state | Betydelse | Vad du gör |
|---|---|---|
assessed_clear | Bedömt; beteendet matchar innehavaren | Ingenting |
assessed_alert | Bedömt; beteendet avviker | Step-up — larmet account_takeover bär samma verdikt |
not_assessed | Inget verdikt fattades | Behandla det inte som rent; fall tillbaka på dina egna signaler |
reason (endast med not_assessed):
reason | Betydelse |
|---|---|
profile_not_ready | Innehavarens referens kan för närvarande inte ge ett verdikt för det här kontot. Behandla det som ett tillstånd, inte ett steg: det kanske aldrig ändras, så vänta inte |
profile_learning | Referensen håller fortfarande på att byggas upp |
window_too_sparse | En person fanns där, men fönstret innehöll för lite interaktion |
not_computable | Fönstret och referensen hade inga informativa axlar gemensamt |
bot_traffic | Det fanns tillräckligt med data, och det fanns ingen person att bedöma |
Sedan 2026-10-04 18:00 UTC utfärdas window_too_sparse och not_computable inte
längre för en innehavare vars referens är redo — ett sådant fönster rapporteras som
profile_not_ready. De två orsakerna finns kvar i listan eftersom tidigare leveranser
bär dem: om din handler förgrenar sig på dem, hantera profile_not_ready på samma sätt,
och jämför inte frekvensen för dessa orsaker från före och efter det datumet.
state och reason begränsas inte av prisplan. verdict är Business+, precis som
drift_score.
Blocket verdict finns endast med assessed_clear och assessed_alert. Med
not_assessed saknas blocket i stället för att fyllas med nollor — en score på 0
bredvid en threshold på 0 skulle läsas som ”beteendet matchade exakt”.
| Fält | Betydelse |
|---|---|
severity | low eller high — två band, avsiktligt: ett mellanband skulle kräva en andra tröskel på samma skala |
score | Värdet som verdiktet fattades på |
threshold | Värdet det faktiskt jämfördes mot |
scale | Hur poängen byggs: manhattan-normalized är d/(1+d) i ett normaliserat beteenderum, där 0 betyder en exakt matchning |
basis | ring — avståndet till det närmaste av innehavarens tidigare betrodda fönster; centroid — avståndet till innehavarens genomsnittliga profil |
windows | Hur många fönster som aggregerades till poängen |
score och threshold ligger alltid på samma, namngivna skala, så paret återskapar
beslutet: larmet utlöses exakt när score > threshold.
Hur ofta den kommer. Högst en leverans per tillstånd och besök — i praktiken en per
besök som bär ett linked_id, inte en per beteendefönster. Ett tillstånd kan ändras
inom ett besök — till exempel blir referensen färdiginlärd och not_assessed blir
assessed_clear — och då får du båda, i ordning. Den övergången är möjlig, inte
utlovad: profile_not_ready kan bestå under kontots hela livstid.
Vad den inte lovar. Bäst skyddade är aktiva användare med långa sessioner och en fullständig referensprofil; sämst skyddade är sällananvändare, för vilka en kapning är farligare. Detta är en egenskap hos konstruktionen, inte en tröskel som går att justera. Händelsen rapporterar en beteendeavvikelse från en referens — den gör inte anspråk på att fånga kontokapning i allmänhet.
attack_detectedLarmet om bottoppar kommer i det gemensamma v3-kuvertet med ett attack-block. Det har
inga identifieringsfält — det är en händelse på workspace-nivå:
{ "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 på payload-version 1 får det historiska platta larmschemat — det ändras inte.
login_risk, trusted_device_enrolled och trusted_device_revokedDe här tre händelserna kommer från dina egna anrop till API:et för inloggningsrisk
(POST /v2/logins/assess) och dess betrodda enheter. De levereras endast vid en
uttrycklig prenumeration med namn — * omfattar dem inte — och endast till webhooks
på payload-version 2 eller 3; version 1 har ingen body för dem.
login_risk skickas när en bedömning returnerar challenge eller review — aldrig
vid allow, deny eller not_assessed. Dess login_risk-block är den lagrade
bedömningen, exakt som det synkrona svaret gav den. Händelse-id: login-risk:<id>,
där <id> är vår identifierare för den lagrade posten — ta ditt eget assessment_id
från kroppen.trusted_device_enrolled och trusted_device_revoked skickas vid varje
registrering, förnyelse och återkallelse av en betrodd enhet. Deras
trusted_device-block — user_id, device_ref, enrolled_at, expires_at,
revoked_at, revoked_by (api eller dashboard) — är en ögonblicksbild vid
tidpunkten för händelsen; med trusted_device_enrolled är revoked_at och
revoked_by null. Händelse-id: trusted-device:<n>.De skickas asynkront, upp till ungefär fem sekunder efter anropet, och bär ingen header
X-Tracio-Request-Id — använd login_risk.request_id för att hitta besöket. Webhooken
är en avisering för revision och larm; själva beslutet är det synkrona svaret.
{ "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_crossedLevereras endast vid en uttrycklig prenumeration med namn — * omfattar den inte —
och endast till webhooks på payload-version 2 eller 3. Var 15:e minut räknar TRACIO om
kvalitetspoängen för varje affiliate med tillräckligt med data och skickar händelsen
när dess rekommendation försämras: none → review, none → hold eller
review → hold. En förbättring skickar ingenting och gör att nästa försämring kan
rapporteras igen. Händelsen hör till workspacet: den går till de prenumererande
webhooks i varje miljö och bär ingen header X-Tracio-Request-Id. Händelse-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 är affiliatens andelar under de senaste 90 dagarna, samma som rapporten i
dashboarden visar: andelen registreringar som nådde en första insättning
(reg_to_ftd), andelen spelare som fortfarande är aktiva 14 dagar efter registreringen
(active14_share) samt andelarna konverteringar med en dubblerad enhet eller person,
från en bedrägeriring, från bonusjägare, från ett datacenter, VPN eller en bot, märkta
som chargeback eller som affiliatebedrägeri, och utan något hittat besök. TRACIO ger
bara rekommendationer — själva innehållandet och varje beslut om utbetalning stannar i
din affiliateplattform. På payload-version 2 har samma body nycklar i camelCase: blocket
är affiliateThreshold, med affiliateId, previousRecommendation, qualityScore
och så vidare.
transaction_state_changedFrån Business och uppåt. Skickas när resultatet för en transaktion som du skickat till
POST /v2/transactions ändras: reglerna beslutade REVIEW eller DECLINE (ett
APPROVE skickar ingenting), en analytiker skickade transaktionen till granskning i
dashboarden, eller en etikett sattes, ersattes eller togs bort. Använd den för att hålla
inne en bonus medan ett anspråk granskas och för att dra in eller släppa den när
resultatet är avgjort, utan polling. * omfattar den här händelsen; den levereras
endast till webhooks på payload-version 2 eller 3. En webhook som är bunden till en
miljö tar emot den endast för den miljön.
{ "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 }}| Nyckel | Betydelse |
|---|---|
effective_state | Resultatet att agera på: analytikerns eller etikettens tillstånd när ett sådant är satt, annars state |
recommendations | Vad du ska göra med bonusen, härlett ur effective_state |
label, label_source, label_at | Den aktuella etiketten, var den sattes (api eller dashboard) och när; alla tre är null när transaktionen saknar etikett |
previous_effective_state | effective_state före den här ändringen; null för händelsen som skickas vid det automatiska beslutet |
Blocket visar transaktionen så som den är när händelsen skickas: om två ändringar
följer tätt på varandra bär båda händelserna det senaste tillståndet, var och en med
sitt eget previous_effective_state och timestamp. Ordningen garanteras inte —
sortera på timestamp och läs transaktionen via API:et för dess aktuella tillstånd.
Händelse-id: txn-state:<id>:created för det automatiska beslutet och
txn-state:<id>:<n> för varje senare ändring. Händelsen säger inte vem som satte en
etikett — bara var den kom ifrån.
reputation_changedFrån Business och uppåt. Skickas när det kundöverskridande ryktet för en person som din
organisation har sett flyttas till ett annat band (till exempel clean →
suspicious). Den kräver personlagret, som för närvarande körs i observationsläge — så
händelsen utlöses bara för workspaces där identitetsupplösning över flera enheter är
aktiverad. Det här är en händelse på personnivå: den har inget request_id och inga
besöksblock.
{ "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 }}Blocknyckeln är reputation_change, inte reputation: den senare är upptagen av ryktet
i samband med ett specifikt besök, och de två är olika saker. Schema 1 levererar den
historiska platta formen — samma fält på översta nivån.
Varje leverans innehåller en X-Tracio-Signature-header:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t är Unix-tidsstämpeln (i sekunder) då begäran signerades.v1 är den hex-kodade HMAC-SHA256:n av "<t>.<rawRequestBody>", nycklad med din
webhook-hemlighet.Tidsstämpeln ingår i det signerade innehållet, vilket ger skydd mot replay.
Två saker måste bli rätt, annars misslyckas verifieringen i produktion:
v1=-värde som helst. Under en hemlighetsrotation bär
headern två signaturer, och en parser som bara behåller en av dem avvisar giltiga
leveranser under hela rotationsfönstret.// 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")})Leveranser enligt schema 2 och 3 bär dessutom X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Båda sidor känner till HMAC-hemligheten, så HMAC:en
bevisar att avsändaren känner till hemligheten, men inte att begäran kommer från
TRACIO; det gör den asymmetriska signaturen. De publika nycklarna publiceras på
https://api.tracio.ai/.well-known/webhook-keys, indexerade på kid: läs fältet
public_key. Det äldre aliaset publicKey bär samma värde och tas bort den
2027-04-12.
Testleveranser som skickas från dashboarden signeras enbart med HMAC — plattformens
privata nyckel finns på leveransnoderna och är avsiktligt inte tillgänglig för
dashboarden. En verifierare som hårt kräver Ed25519 måste släppa igenom
testleveranser (de bär suffixet :test på event_id), annars misslyckas testerna
från dashboarden medan produktionen mår bra. Samma försiktighet gäller
formatkontroller: en testleverans bär request_id på formen test_<hex> och den
bokstavliga strängen test_visitor som visitor_id, så en handler som validerar dem
mot produktionsformerna avvisar en leverans som i övrigt är helt korrekt.
Efter en rotation förblir båda hemligheterna giltiga i 24 timmar och headern bär båda signaturerna, så du kan uppdatera din konfiguration utan att förlora leveranser. Åtgärden Revoke now kortar fönstret. Uppdatera hemligheten på din sida inom 24 timmar: när fönstret stängs slutar den gamla hemligheten att matcha, och om din endpoint svarar på en ogiltig signatur med 4xx stänger fem sådana svar i rad av webhooken.
| Header | Beskrivning |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — två v1= under ett rotationsfönster |
X-Tracio-Signature-Ed25519 | Plattformssignatur, t=<unix>,kid=<id>,v1=<base64> (endast v2 och v3) |
X-Tracio-Event-Id | Identifierare för leveransen — idempotensnyckeln |
X-Tracio-Request-Id | Identifierare för besöket, matchar request_id (endast v2 och v3). Skickas bara för besökshändelser — identification, account_takeover och account_assessment |
X-Tracio-Event-Type | Händelsetypen (endast v2 och v3) |
X-Tracio-Delivery-Attempt | Försöksnummer, med start på 1 (endast v2 och v3) |
X-Tracio-Payload-Version | 3 eller 2 (endast v2 och v3) |
X-Tracio-Webhook-Id | Identifierare för webhooken som producerade denna leverans |
Leveranser kan göras om, och ett omförsök bär samma X-Tracio-Event-Id. Deduplicera på
den:
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")})Observera att event_id är unikt per händelse, inte per webhook: om flera webhooks
i workspacet prenumererar på samma händelse får var och en en leverans med samma
identifierare. För identification byggs det som <request_id>:<phase>, vilket är
skälet till att ett besöks tre faser dedupliceras oberoende av varandra i stället för
att falla ihop till en enda. Händelser utan besök har egna former — se respektive
händelse ovan.
Svara med 2xx — det är det enda tecknet på att en leverans har tagits emot.
| Svar | Vad som händer |
|---|---|
2xx | Leveransen slutförd |
429 Too Many Requests | Räknas inte som misslyckande och förbrukar inget försök; ett längre Retry-After respekteras |
408, 425, 5xx, bruten anslutning | Görs om med växande paus |
410 Gone | Endpointen behandlas som borttagen — webhooken stängs av omedelbart |
Övriga 4xx | Görs om, men fem i rad stänger av webhooken — 400/401/404 botas inte av omförsök |
Omförsöksschema: 5 s → 30 s → 2 min → 10 min → 30 min → 2 h → 6 h (8 försök). De första omförsöken ryms inom en minut, så en kort omstart av din tjänst kostar dig ingen avisering. Varje paus randomiseras mellan halva och hela värdet så att omförsöken inte avfyras i en enda salva efter ett avbrott.
Automatisk avstängning kräver både ett tröskelvärde (20 misslyckanden i följd, eller 5 konfigurationsfel) och minst 15 sammanhängande minuter av misslyckanden — en kort omstart kan inte döda integrationen ens om många leveranser låg i kö. Ett glapp längre än 15 minuter startar om räkningen. Dashboarden visar orsaken, med svarskod och feltext, samt en knapp Re-enable som nollställer räknarna.
| Prisplan | Webhooks per workspace |
|---|---|
| Free | Ej tillgängligt |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoints måste vara https med publik IP — privata adresser och loopback-adresser
avvisas, även vid en omdirigering — och inte djupare än två omdirigeringar.
Endast omdirigeringarna 307 och 308 följs. 301, 302 och 303 instruerar
klienten att byta till GET och kasta bort bodyn, så en leverans följer dem inte och
försöket räknas som misslyckat. Om din lastbalanserare normaliserar URL:en (lägger till
www eller ett avslutande snedstreck), peka webhooken direkt på den slutliga URL:en.
Webhooks hanteras i dashboarden. Dashboarden driver ett workspace-avgränsat
hanterings-API som tillhandahålls på applikationsvärden (till exempel
https://app.tracio.ai/api/v1), och endpointerna nedan är just de den anropar. Alla
webhook-endpoints ligger under /workspaces/{wsId}.
Detta är ingen server-till-server-yta. Hanterings-API:et accepterar bara din dashboard-session-JWT, kontrollerad mot din workspace-roll (RBAC); en hemlig nyckel av typen
tracio_sk_…avvisas här. Eftersom den sessionen lever i webbläsaren och upphör med den bör du se anropen nedan som en beskrivning av vad dashboarden gör, snarare än som en integration att automatisera. För programmatisk åtkomst från din egen backend, använd det skrivskyddade Data-API:et.
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": [] }'Signeringshemligheten genereras av TRACIO och returneras en gång vid skapandet (och
vid rotation) under signingSecret. Förvara den säkert — det är nyckeln du använder för
att verifiera signaturer.
{ "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" }}Vid senare läsningar är signingSecret maskerad (null) — den avslöjas endast av
skapande och rotation av hemligheten.
| Metod | Sökväg | Beskrivning |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Lista webhooks |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Uppdatera url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Ta bort en webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Skicka en signerad testleverans |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotera signeringshemligheten |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Lista senaste leveransförsök |
Returnera ett 2xx så snabbt som möjligt och bearbeta payloaden asynkront för att
undvika timeouts:
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) }}Använd åtgärden Test på en webhook (eller POST .../webhooks/{webhookId}/test) för
att skicka en signerad exempel-payload till din endpoint och bekräfta att den är
nåbar och verifierar signaturer korrekt.
För lokal utveckling exponerar du din server med en tunnel som ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint