Webhook-urile livrează evenimentele de identificare pe serverul tău în timp
real. De fiecare dată când un vizitator este identificat, TRACIO trimite o cerere HTTP
POST către URL-ul de webhook configurat. Corpul cererii este payload-ul
evenimentului.
Ele sunt totodată singurul canal care livrează verdictele târzii — acelea în care comportamentul unui vizitator a dovedit că era automatizat după ce pagina se încărcase deja.
Le configurezi în dashboard, la Settings → Webhooks. Webhook-urile necesită planul Pro sau mai mare.
| Eveniment | Când | Plan |
|---|---|---|
identification | La fiecare vizită — fazele primary, late și correction | Toate |
account_takeover | Comportamentul dintr-un cont nu mai corespunde profilului titularului | Business+ |
account_assessment | Starea evaluării de preluare a contului pentru un cont: evaluat și fără abateri, evaluat cu abateri sau neevaluat | Business+ |
attack_detected | Un vârf de boți pe site-ul tău | Business+ |
reputation_changed | Reputația unei persoane asociate a trecut într-o altă bandă | Business+ |
transaction_state_changed | Rezultatul unei tranzacții trimise la POST /v2/transactions s-a schimbat | Business+ |
login_risk | O evaluare a autentificării prin POST /v2/logins/assess s-a încheiat cu challenge sau review | Toate |
trusted_device_enrolled | Un dispozitiv a devenit de încredere pentru un cont sau încrederea în el a fost reînnoită | Toate |
trusted_device_revoked | Încrederea într-un dispozitiv a fost revocată — prin API sau din dashboard | Toate |
affiliate_threshold_crossed | Recomandarea pentru un afiliat s-a înrăutățit (none → review, none → hold, review → hold) | Toate |
Numele evenimentelor folosesc underscore, niciodată puncte — nu există
visitor.created sau session.created.
Un webhook se abonează la tipuri anume; valoarea separată * înseamnă „orice tip,
inclusiv cele adăugate ulterior”. Cinci tipuri sunt excepția: * nu include
account_assessment, login_risk, trusted_device_enrolled,
trusted_device_revoked și affiliate_threshold_crossed — numește-le pe fiecare
explicit. account_assessment raportează acoperirea, nu o constatare, așa că
sosește aproximativ o dată per vizită care poartă un linked_id — un alt ordin de
volum decât celelalte evenimente. Tipurile de risc la autentificare și de afiliere au
sens doar pentru o integrare care apelează acele API-uri. Un wildcard setat înainte
să existe aceste evenimente nu reprezintă consimțământul de a le primi.
Un tip necunoscut este respins cu 400 la crearea sau editarea unui abonament, așa
că o greșeală de tastare nu te poate lăsa cu un webhook care nu se declanșează
niciodată, în tăcere.
identificationO singură vizită produce până la trei livrări care împart același request_id:
primary — verdictul inițial, la încărcarea paginii.late — îmbogățire după aproximativ nouă secunde, odată ce verificările lente au sosit.correction — o corecție bazată pe comportament (cursor, tastatură, derulare).Corelează-le după request_id și deosebește-le după phase. Faza ulterioară are
prioritate: dacă primary a spus human, iar correction spune bot, a doua este
răspunsul corect.
Nu te baza pe ordinea sosirii. Fiecare fază este livrată independent și cu
propriul program de reîncercări — dacă primary a intrat în reîncercare în timp ce
late a reușit din prima, le vei primi în ordine inversă. Stabilește prioritatea din
câmpul phase, nu din momentul recepției.
Acestea trei sunt singurele faze ale unui eveniment identification. O singură altă
valoare ajunge la tine: account_takeover și account_assessment poartă
phase: "beacon", pentru că ambele sunt ridicate din beacon-ul comportamental al
sesiunii.
Observă nepotrivirea pe care o creează acest lucru, fiindcă afectează idempotența. O
livrare identification din producție are event_id exact <request_id>:<phase>,
dar alte livrări încalcă formula. Un account_takeover este <request_id>:ato —
sufixul este literalul ato, nu valoarea câmpului phase — iar un
account_assessment este <request_id>:ato-state-<state>. O livrare de test trimisă
din dashboard este <request_id>:test, în timp ce phase din corpul ei arată tot
primary — iar un corp în schema 1 nu are deloc câmpul phase, așa că antetul este
singurul loc în care apare acel sufix. Folosește event_id direct drept cheie de
idempotență și nu îl reconstrui niciodată din request_id și phase. Potrivește-te
pe valorile pe care le tratezi și ignoră-le pe celelalte, în loc să respingi
livrarea.
Unele evenimente nu au vizită: attack_detected (la nivel de workspace),
reputation_changed (la nivel de persoană), transaction_state_changed (la nivel de
tranzacție), login_risk, trusted_device_enrolled și trusted_device_revoked (la
nivel de cont) și affiliate_threshold_crossed (la nivel de afiliat). Nu au
request_id, nici visitor_id și niciunul dintre blocurile browser, geo, bot
sau decision — acele chei pur și simplu lipsesc, la fel ca antetul
X-Tracio-Request-Id. account_takeover și account_assessment, în schimb, sunt
produse de o vizită anume și poartă corpul de identificare complet aferent planului
tău. Dacă parsezi toate evenimentele într-un singur handler, verifică
event înainte de a atinge câmpuri de vizită.
Webhook-urile noi folosesc versiunea de payload 3. Webhook-urile existente pe versiunea 1 sau 2 își păstrează versiunea aleasă; ambele scheme mai vechi sunt înghețate și vor fi eliminate pe 2027-04-12. Pentru migrare, selectează versiunea 3 pe cardul webhook-ului din dashboard.
| Versiune | Stare |
|---|---|
1 | Înghețată; eliminată pe 2027-04-12 |
2 | Înghețată; eliminată pe 2027-04-12 |
3 | Valoarea implicită pentru webhook-urile noi; chei snake_case |
Versiunea 3 poartă aceleași câmpuri și valori ca versiunea 2, cu chei snake_case și
un version: 3 la nivel superior. Algoritmul de semnare și antetele nu se schimbă
(X-Tracio-Payload-Version: 3). Cheile din propriul dicționar fields sunt datele
tale și nu sunt redenumite.
| 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", // modelul plăcii video, normalizat; absent când este necunoscut "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 // traficul HTTP și traseul de rețea brut ies prin rețele diferite }, "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}Valorile zero și cele goale sunt omise. Câmpurile de tip șir și numerice cu
valoare zero (bot.type pentru un om sau bot.antidetect_score: 0, de exemplu)
lipsesc din JSON — nu le face obligatorii în schemele tale și citește
blocurile imbricate defensiv.
bot.score și decision.risk_score sunt zecimale pe scara 0..100, cu o cifră
după virgulă — exact numerele pe care dashboard-ul și Data API le raportează pentru
aceeași vizită. (În schema înghețată v1 folosesc alte unități: o fracție 0..1,
respectiv 0..255.)
bot.type este fie numele unui bot recunoscut, fie un agent AI verificat
(ai_agent:<vendor>), fie o etichetă de familie. Vezi
Tipuri de bot pentru vocabular — numele interne ale
verificărilor nu sunt expuse niciodată, la niciun plan. ai_agent:<vendor> înseamnă
că cererea a dovedit că provine din infrastructura acelui furnizor — o semnătură Web
Bot Auth validă, făcută cu o cheie pe care furnizorul o publică, sau o adresă din
intervalele IP pe care furnizorul le publică — și nu doar că pretinde acest lucru în
user agent. O cerere care pretinde că este un agent AI și este contrazisă de fiecare
dovadă pe care furnizorul o publică este raportată ca impersonation.
| Câmp | Tip | Descriere |
|---|---|---|
version | number | Versiunea schemei de payload (3) |
event | string | Tipul evenimentului |
event_id | string | Identificatorul livrării — cheia de idempotență; unic per eveniment și comun tuturor webhook-urilor tale care îl primesc, stabil între reîncercări |
request_id | string | Identificatorul vizitei (UUID), comun tuturor fazelor ei |
phase | string | primary, late, correction; account_takeover și account_assessment poartă beacon |
visitor_id | string | Identificator stabil de vizitator |
linked_id | string | Identificator asociat, furnizat de client |
tag | string | Etichetă personalizată, furnizată de client |
fields | object | Câmpuri personalizate atașate vizitei în momentul colectării. Doar ce a sosit odată cu vizita — un câmp atașat ulterior prin Data API nu apare niciodată într-un webhook, la nicio fază sau reîncercare. Cheile și valorile sunt șiruri. Absent când vizita nu a purtat câmpuri |
timestamp | string | Ora evenimentului (RFC 3339) |
url | string | URL-ul paginii unde a fost captat evenimentul |
ip | string | Adresa IP a clientului |
user_agent | string | Șirul user-agent brut al clientului |
browser.name / .version | string | Browserul detectat |
os.name / .version | string | Sistemul de operare detectat |
device | string | Clasa dispozitivului (de ex. desktop, mobile) |
gpu | string | Modelul plăcii video așa cum îl raportează browserul (WebGL), normalizat la un nume lizibil (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer pentru rasterizatoarele software; absent când este necunoscut |
geo | object | Geolocalizare după IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (booleeni) și connection_type |
network.proxy_detected | boolean | Traficul HTTP al vizitei și traseele ei de rețea brute ies prin rețele diferite — un proxy sau un VPN în fața browserului. Două adrese ale aceluiași furnizor (NAT de operator, o a doua ieșire a aceluiași VPN) nu se pun la socoteală |
bot.result | string | human, bot sau uncertain |
bot.type | string | Numele botului, agentul AI verificat sau eticheta de familie când este detectat un bot |
bot.score | number | Scor de bot (0–100) |
identification.confidence | number | Încrederea identificării (0.0–1.0) |
identification.incognito | boolean | Context de navigare privată/incognito |
decision.action | string | real, fake sau suspicious |
decision.risk_score | number | Scor de risc agregat (0–100) |
extensions | array | Extensiile de browser detectate în această vizită, ca slug-uri stabile pentru mașini (numele, categoriile și linkurile din magazin sunt în dashboard și în Data API). Absent când nu s-a detectat nimic |
Pro și peste — cum se comportă vizitatorul în timp:
{ "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}Business și peste — de ce verdictul a ieșit așa cum a ieșit:
{ "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, // adresa publică observată pe traseul de rețea brut, adică adresa din // spatele proxy-ului sau al VPN-ului; absentă când nu a fost observată // nicio astfel de adresă "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // Mediul desktop MĂSURAT pe o mașină Linux ("Mint 22+", "Ubuntu", "GNOME", // "KDE"); un User-Agent nu poate exprima o distribuție. Absent când nu // este determinat — la majoritatea vizitelor Linux și la fiecare vizită // care nu e Linux. "os_environment": "Mint 22+", // Ce a pretins vizita despre sine față de ce au măsurat verificări // independente. Prezent doar când o falsificare chiar a fost detectată; un // câmp `real` gol înseamnă "verificarea a rămas tăcută", niciodată // "confirmat". Pe axa `gpu`, `claimed.gpu` poartă placa pretinsă sub // același nume de model lizibil ca și câmpul `gpu` de nivel superior. "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"] } }, // Fapte despre dispozitiv — ce raportează browserul vizitatorului despre // mașină, curățat de partea noastră. screen: rezoluția, adâncimea de // culoare și device pixel ratio. locale: limbile preferate și fusul orar // ale browserului însuși — spre deosebire de geo.timezone, care este // derivat din adresa IP; o nepotrivire între cele două este un semn // frecvent al unei locații falsificate. client_hints: User-Agent Client // Hints — arhitectura și bitness-ul procesorului, modelul dispozitivului // (Android: codul de model din `model`, de ex. "SM-A556B", și numele său // comercial din lista de dispozitive Google Play din `device_name`, de ex. // "Samsung Galaxy A55 5G") și versiunea exactă a platformei; le raportează doar // browserele bazate pe Chromium. Fiecare bloc lipsește când vizita nu a // purtat astfel de date, așa că tratați-l pe fiecare drept opțional. "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" }, // Prezent doar când placa video s-a declarat ea însăși virtuală; // hypervisor este un dicționar închis (vmware, virtualbox, parallels, // qemu, hyperv, bochs, intel-gvt, vgpu). Un bloc lipsă înseamnă că nu // există un astfel de indiciu. "environment": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}Vezi Detectarea boților pentru vocabularul
codurilor de motiv și pentru ce înseamnă severity.
Câmpurile de multi-conturi (Business și peste). Odată ce modul persoană este
activat pentru workspace-ul tău, corpul poartă și person_id, un bloc
reputation (score, categories, overall_risk), linked_accounts_count și
linked_visitors_count — dacă vizitatorul este legat de alți vizitatori și conturi.
Stratul de persoană rulează deocamdată în mod de observare și, până când devine activ,
aceste câmpuri lipsesc din corp. Nu le face obligatorii în schemele tale.
guidance poartă recomandări gata făcute de tipul „ce să faci” pentru fiecare punct
de integrare, astfel încât să nu fii nevoit să deduci o politică din scoruri brute.
Este prezent în fiecare eveniment v3 Business+ — allow este declarat explicit:
{ "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 }}Fiecare scenariu pornește de la allow și se poate deplasa doar în sus pe scară:
allow → challenge → review → deny. În cadrul unui scenariu câștigă axa cea mai
strictă care se declanșează, iar overall este cea mai strictă dintre toate cele patru
scenarii.
| Recomandare | Plată | Înregistrare | Autentificare | Afiliere |
|---|---|---|---|---|
allow | Proceseaz-o | Creează-l | Lasă-l să intre | Creditează conversia |
challenge | 3-D Secure / confirmare | Captcha, confirmare pe e-mail sau telefon | 2FA suplimentar, reautentificare | Marcheaz-o ca îndoielnică până apare activitate |
review | Procesează, dar pune-o la coadă pentru revizuire | Creează-l cu restricții | Lasă-l să intre și ridică o alertă | Reține plata până la revizuire |
deny | Nu procesa tranzacția | Refuză crearea contului | Nu îl lăsa să intre | Nu credita conversia |
basis enumeră axele care au determinat recomandarea — o axă a cărei propunere a
fost mai blândă decât cea adoptată nu intră în listă. Citește-o după cod, în loc să
presupui un set fix:
Axă (basis) | Ce înseamnă |
|---|---|
list | Vizitatorul s-a potrivit cu una dintre listele workspace-ului tău în modul enforce. O listă allow are prioritate față de fiecare axă: fiecare scenariu devine allow, iar basis este exact ["list"] |
rule | Vizita s-a potrivit cu una dintre regulile workspace-ului tău în modul enforce; o regulă mută doar recomandarea propriului scenariu (o regulă allow îl înmoaie doar pe acela) și este sărită când o listă a acționat deja |
bot | Detectorul de boți a concluzionat că este automatizare — refuză fiecare scenariu |
risk | Decizia și scorul de risc general al vizitei |
behavior | Biometria comportamentală indică bot cu suficientă încredere — adaugă fricțiune, nu refuz |
account_takeover | Comportamentul din acest cont nu mai corespunde profilului titularului — autentificarea trece la challenge, plata la review |
network | Tor sau un VPN care iese dintr-un datacenter |
affiliate_pattern | Forma recomandărilor autogenerate — conversia de afiliere este reținută ca challenge |
fraud_reputation, multiaccounting | Reputația și gruparea conturilor persoanei din spatele vizitatorului; apar odată ce stratul de persoană devine activ |
version este versiunea setului de reguli — este incrementată pe măsură ce logica se
îmbunătățește, așa că păstreaz-o în loguri. Versiunea 2 a introdus axa list, prima
care poate înmuia o recomandare; versiunea 3 a mutat pragurile de autentificare
account_takeover pe scara scorului verdictului de preluare a contului, așa că aceeași
vizită poate primi deny acolo unde versiunea 2 dădea challenge, și invers.
Guidance este aditiv: scenariile noi apar ca chei noi, fără a strica contractul.
Câștigă faza ulterioară, cu excepția recomandărilor parțiale: o livrare calculată
pe un set incomplet de intrări este marcată cu "partial": true, iar o recomandare
parțială nu înlocuiește o recomandare completă primită mai devreme pentru același
request_id. Într-o livrare obișnuită, câmpul partial lipsește cu totul.
Pragurile exacte nu sunt documentate în mod deliberat. O recomandare care poate fi
supusă ingineriei inverse până la un scor încetează să mai fie o apărare. Există exact
o excepție deliberată: account_assessment.verdict.threshold, granița de decizie a
verdictului de preluare a contului descris mai jos.
account_takeoverDoar Business și Enterprise; la planurile inferioare nu se creează nicio livrare,
chiar dacă un webhook este abonat la acest eveniment. Corpul este plicul de
identificare complet aferent planului tău plus un bloc account_alert și
blocul account_assessment descris în secțiunea următoare, livrat cel mult o dată
per vizită:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // alerta este ridicată dintr-un beacon; doar event_id spune "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}De ce type poartă o versiune. Construcția din spatele scorului s-a mai schimbat:
scara și baza verdictului. behavior-drift/2 este prima construcție versionată. Dacă
ți-ai calibrat propriile reguli pe acest număr, tratează o schimbare a acestei valori ca
pe un semnal să le reverifici.
account_alert.drift_score și identification.drift_score sunt același număr
(din 2026-10-03): scorul comportamental netezit pe care s-a luat verdictul, măsurat
față de inelul de ferestre de încredere anterioare ale titularului contului. Înainte
de acea dată, identification.drift_score purta o distanță brută pe o altă scară — nu
amesteca valori de o parte și de alta a datei. Când o fereastră nu a fost evaluată,
câmpul lipsește, în loc să fie zero. Folosește account_assessment.verdict când ai
nevoie de număr împreună cu pragul cu care a fost comparat.
În v1 blocul este accountAlert, cu type, linkedId și drift, și nu există un
bloc de evaluare. În v2 alerta redenumește două câmpuri — linkedId → accountId și
drift → driftScore — și adaugă accountAssessment; v3 folosește numele snake_case
de mai sus. Actualizează-ți handler-ul când comuți versiunea, altfel logica
ta de preluare a conturilor va înceta în tăcere să mai vadă datele.
account_assessmentDoar Business și Enterprise. Necesită un abonament explicit: * nu îl include.
Fără acest eveniment, absența unei alerte înseamnă două lucruri diferite în același timp: fie am evaluat sesiunea față de profilul titularului contului și s-a potrivit, fie nu am putut-o evalua deloc — profilul titularului nu este gata, fereastra a fost prea rară sau traficul nu era al unei persoane. Ambele arată ca tăcere, iar tăcerea se citește ca siguranță. De obicei nu este: într-o zi completă de producție din septembrie 2026 ne-am abținut de la verdict pentru 73% dintre ferestre și 80% dintre conturi. Un proces construit pe „nicio alertă înseamnă că e în regulă” este construit pe o stare pe care, în cea mai mare parte, nu o avem.
{ "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 nu lipsește niciodată și nu este niciodată gol:
state | Ce înseamnă | Ce să faci |
|---|---|---|
assessed_clear | Evaluat; comportamentul corespunde titularului | Nimic |
assessed_alert | Evaluat; comportamentul diverge | Step-up — alerta account_takeover poartă același verdict |
not_assessed | Nu s-a dat niciun verdict | Nu îl trata drept curat; revino la propriile semnale |
reason (doar cu not_assessed):
reason | Ce înseamnă |
|---|---|
profile_not_ready | Referința titularului nu poate produce în prezent un verdict pentru acest cont. Trateaz-o ca pe o stare, nu ca pe o etapă: s-ar putea să nu se schimbe niciodată, așa că nu aștepta |
profile_learning | Referința este încă în curs de acumulare |
window_too_sparse | O persoană a fost prezentă, dar fereastra a purtat prea puțină interacțiune |
not_computable | Fereastra și referința nu au avut axe informative în comun |
bot_traffic | Au existat suficiente date și nu a existat nicio persoană de evaluat |
Din 2026-10-04 18:00 UTC, window_too_sparse și not_computable nu mai sunt emise
pentru un titular a cărui referință este gata — o astfel de fereastră este raportată
ca profile_not_ready. Cele două motive rămân în listă pentru că livrările anterioare
le poartă: dacă handler-ul tău se ramifică după ele, direcționează
profile_not_ready la fel și nu compara ratele acestor motive de o parte și de alta
a acelei date.
state și reason nu depind de plan. verdict este Business+, ca și drift_score.
Blocul verdict este prezent doar cu assessed_clear și assessed_alert. Cu
not_assessed blocul lipsește, în loc să fie completat cu zerouri — un score de 0
lângă un threshold de 0 s-ar citi ca „comportamentul s-a potrivit exact”.
| Câmp | Ce înseamnă |
|---|---|
severity | low sau high — două benzi, în mod deliberat: o bandă intermediară ar avea nevoie de un al doilea prag pe aceeași scară |
score | Valoarea pe care s-a luat verdictul |
threshold | Valoarea cu care a fost efectiv comparat |
scale | Cum este construit scorul: manhattan-normalized este d/(1+d) într-un spațiu comportamental normalizat, unde 0 înseamnă potrivire exactă |
basis | ring — distanța până la cea mai apropiată dintre ferestrele de încredere anterioare ale titularului; centroid — distanța până la profilul lor mediat |
windows | Câte ferestre au fost agregate în scor |
score și threshold sunt întotdeauna pe aceeași scară, numită, așa că perechea
reproduce decizia: alerta se declanșează exact când score > threshold.
Cât de des sosește. Cel mult o livrare per stare per vizită — în practică una per
vizită care poartă un linked_id, nu una per fereastră comportamentală. O stare se
poate schimba în cadrul unei vizite — de exemplu, referința termină învățarea și
not_assessed devine assessed_clear — și atunci le primești pe amândouă, în ordine.
Această tranziție este posibilă, nu promisă: profile_not_ready poate persista pe
toată durata de viață a contului.
Ce nu promite. Cel mai bine sunt protejate conturile utilizatorilor activi, cu sesiuni lungi și un profil de referință complet; cel mai slab sunt protejate cele ale utilizatorilor rari, pentru care o preluare este mai periculoasă. Aceasta este o proprietate a construcției, nu un prag care poate fi ajustat. Evenimentul raportează o divergență comportamentală față de o referință — nu pretinde că prinde preluarea conturilor în general.
attack_detectedAlerta de vârf de boți sosește în plicul comun v3, cu un bloc attack. Nu are câmpuri
de identificare — este un eveniment la nivel de workspace:
{ "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 }}Webhook-urile pe versiunea de payload 1 primesc schema istorică plată a alertei — aceasta nu se schimbă.
login_risk, trusted_device_enrolled și trusted_device_revokedAceste trei evenimente provin din propriile tale apeluri către API-ul de risc
la autentificare (POST /v2/logins/assess) și din dispozitivele sale de încredere.
Sunt livrate doar la un abonament explicit după nume — * nu le include — și doar
către webhook-urile pe versiunea de payload 2 sau 3; versiunea 1 nu are un corp
pentru ele.
login_risk este trimis când o evaluare returnează challenge sau review —
niciodată la allow, deny sau not_assessed. Blocul său login_risk este
evaluarea stocată, exact așa cum a dat-o răspunsul sincron. Id-ul evenimentului:
login-risk:<id>, unde <id> este identificatorul nostru al înregistrării stocate —
ia propriul assessment_id din corp.trusted_device_enrolled și trusted_device_revoked sunt trimise la fiecare
înrolare, reînnoire și revocare a unui dispozitiv de încredere. Blocul lor
trusted_device — user_id, device_ref, enrolled_at, expires_at,
revoked_at, revoked_by (api sau dashboard) — este un instantaneu din momentul
evenimentului; la trusted_device_enrolled, revoked_at și revoked_by sunt
null. Id-ul evenimentului: trusted-device:<n>.Sunt trimise asincron, la cel mult aproximativ cinci secunde după apel, și nu poartă
antetul X-Tracio-Request-Id — folosește login_risk.request_id pentru a găsi vizita.
Webhook-ul este o notificare pentru audit și alertare; decizia propriu-zisă este
răspunsul sincron.
{ "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_crossedLivrat doar la un abonament explicit după nume — * nu îl include — și doar către
webhook-urile pe versiunea de payload 2 sau 3. La fiecare 15 minute, TRACIO
recalculează scorul de calitate al fiecărui afiliat cu suficiente date și trimite
evenimentul când recomandarea acestuia se înrăutățește: none → review, none → hold
sau review → hold. O îmbunătățire nu trimite nimic și permite ca următoarea
înrăutățire să fie raportată din nou. Evenimentul aparține workspace-ului: ajunge la
webhook-urile abonate din fiecare mediu și nu poartă antetul X-Tracio-Request-Id.
Id-ul evenimentului: 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 sunt ponderile afiliatului din ultimele 90 de zile, aceleași pe care le arată
raportul din dashboard: ponderea înregistrărilor care au ajuns la un prim depozit
(reg_to_ftd), ponderea jucătorilor încă activi la 14 zile după înregistrare
(active14_share) și ponderile conversiilor cu un dispozitiv sau o persoană duplicat,
dintr-un inel de fraudă, de la vânători de bonusuri, dintr-un datacenter, VPN sau bot,
etichetate ca chargeback sau ca fraudă de afiliere și fără o vizită găsită. TRACIO doar
recomandă — reținerea însăși și orice decizie de plată rămân în platforma
ta de afiliere. Pe versiunea de payload 2, același corp are chei camelCase:
blocul este affiliateThreshold, cu affiliateId, previousRecommendation,
qualityScore și așa mai departe.
transaction_state_changedBusiness și peste. Trimis când rezultatul unei tranzacții trimise la
POST /v2/transactions se schimbă: regulile au decis REVIEW sau DECLINE (un
APPROVE nu trimite nimic), un analist a trimis tranzacția la revizuire în dashboard
sau o etichetă a fost setată, înlocuită ori eliminată. Folosește-l pentru a reține un
bonus cât timp o cerere este în revizuire și pentru a-l anula sau a-l elibera odată ce
rezultatul este stabilit, fără polling. * include acest eveniment; este livrat doar
către webhook-urile pe versiunea de payload 2 sau 3. Un webhook legat de un mediu îl
primește doar pentru acel mediu.
{ "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 }}| Cheie | Ce înseamnă |
|---|---|
effective_state | Rezultatul după care acționezi: starea stabilită de analist sau de etichetă, când există, altfel state |
recommendations | Ce să faci cu bonusul, derivat din effective_state |
label, label_source, label_at | Eticheta curentă, unde a fost setată (api sau dashboard) și când; toate trei sunt null când tranzacția nu are etichetă |
previous_effective_state | effective_state dinaintea acestei schimbări; null pentru evenimentul trimis la decizia automată |
Blocul arată tranzacția așa cum este în momentul trimiterii evenimentului: dacă
două schimbări se succed rapid, ambele evenimente poartă cea mai recentă stare,
fiecare cu propriul previous_effective_state și timestamp. Ordinea nu este
garantată — sortează după timestamp și citește tranzacția prin API pentru starea ei
curentă. Id-ul evenimentului: txn-state:<id>:created pentru decizia automată și
txn-state:<id>:<n> pentru fiecare schimbare ulterioară. Evenimentul nu spune cine a
setat o etichetă — doar de unde a venit.
reputation_changedBusiness și peste. Trimis când reputația între clienți a unei persoane pe care
organizația ta a văzut-o trece într-o altă bandă (clean → suspicious,
de exemplu). Necesită stratul de persoană, care rulează deocamdată în mod de observare
— așa că evenimentul se declanșează doar pentru workspace-urile în care rezolvarea
identității între dispozitive este activată. Acesta este un eveniment la nivel de
persoană: nu are request_id și nici blocuri de vizită.
{ "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 }}Cheia blocului este reputation_change, nu reputation: aceasta din urmă este ocupată
de reputația în contextul unei vizite anume, iar cele două sunt lucruri diferite.
Schema 1 livrează forma istorică plată — aceleași câmpuri la nivel superior.
Fiecare livrare include un antet X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t este marca de timp Unix (în secunde) la care a fost semnată cererea.v1 este HMAC-SHA256 codificat hexazecimal al "<t>.<rawRequestBody>", cu secretul
tău de webhook drept cheie.Marca de timp face parte din conținutul semnat, ceea ce oferă protecție împotriva reluării.
Două lucruri de făcut corect, altfel verificarea eșuează în producție:
v1=. În timpul unei rotații de
secret, antetul poartă două semnături, iar un parser care păstrează doar una
dintre ele va respinge livrări valide pe toată durata ferestrei de rotație.// 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")})Livrările cu schemele 2 și 3 poartă suplimentar X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Ambele părți cunosc secretul HMAC, așa că HMAC-ul
dovedește că expeditorul cunoaște secretul, dar nu că cererea provine de la TRACIO;
semnătura asimetrică o dovedește. Cheile publice sunt publicate la
https://api.tracio.ai/.well-known/webhook-keys, indexate după kid: citește câmpul
public_key. Aliasul vechi publicKey poartă aceeași valoare și va fi eliminat pe
2027-04-12.
Livrările de test trimise din dashboard sunt semnate doar cu HMAC — cheia privată de
platformă se află pe nodurile de livrare și, în mod deliberat, nu este disponibilă
dashboard-ului. Un verificator care impune strict Ed25519 trebuie să lase livrările
de test să treacă (ele poartă sufixul :test la event_id), altfel testele din
dashboard eșuează în timp ce producția este sănătoasă. Aceeași precauție se aplică și
verificărilor de format: o livrare de test poartă request_id de forma test_<hex> și
literalul test_visitor drept visitor_id, așa că un handler care le validează față
de formele din producție va respinge o livrare altminteri bine formată.
După o rotație, ambele secrete rămân valabile 24 de ore, iar antetul poartă ambele semnături, astfel încât îți poți actualiza configurația fără să pierzi livrări. Acțiunea Revoke now scurtează fereastra. Actualizează secretul de partea ta în 24 de ore: odată ce fereastra se închide, secretul vechi nu se mai potrivește, iar dacă endpoint-ul tău răspunde la o semnătură invalidă cu 4xx, cinci astfel de răspunsuri la rând dezactivează webhook-ul.
| Antet | Descriere |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — două v1= într-o fereastră de rotație |
X-Tracio-Signature-Ed25519 | Semnătură de platformă, t=<unix>,kid=<id>,v1=<base64> (doar v2 și v3) |
X-Tracio-Event-Id | Identificatorul livrării — cheia de idempotență |
X-Tracio-Request-Id | Identificatorul vizitei, corespunde lui request_id (doar v2 și v3). Trimis doar pentru evenimentele de vizită — identification, account_takeover și account_assessment |
X-Tracio-Event-Type | Tipul evenimentului (doar v2 și v3) |
X-Tracio-Delivery-Attempt | Numărul încercării, începând de la 1 (doar v2 și v3) |
X-Tracio-Payload-Version | 3 sau 2 (doar v2 și v3) |
X-Tracio-Webhook-Id | Identificatorul webhook-ului care a produs această livrare |
Livrările pot fi reîncercate, iar o reîncercare poartă același
X-Tracio-Event-Id. Deduplică după el:
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")})Reține că event_id este unic per eveniment, nu per webhook: dacă mai multe
webhook-uri din workspace sunt abonate la același eveniment, fiecare primește o livrare
cu același identificator. Pentru identification este construit ca
<request_id>:<phase>, motiv pentru care cele trei faze ale unei vizite se deduplică
independent, în loc să se contopească într-una singură. Evenimentele fără vizită au
propriile forme — vezi fiecare eveniment de mai sus.
Răspunde cu 2xx — este singurul semn că o livrare a fost acceptată.
| Răspuns | Ce se întâmplă |
|---|---|
2xx | Livrare finalizată |
429 Too Many Requests | Nu se contorizează ca eșec și nu consumă o încercare; un Retry-After mai lung este respectat |
408, 425, 5xx, conexiune întreruptă | Se reîncearcă, cu o pauză tot mai mare |
410 Gone | Endpoint-ul este tratat ca eliminat — webhook-ul este dezactivat imediat |
Alte 4xx | Se reîncearcă, dar cinci la rând dezactivează webhook-ul — 400/401/404 nu se rezolvă prin reîncercare |
Programul reîncercărilor: 5 s → 30 s → 2 min → 10 min → 30 min → 2 h → 6 h (8 încercări). Primele reîncercări încap într-un minut, așa că o repornire scurtă a serviciului tău nu te costă o notificare. Fiecare pauză este randomizată între jumătate și valoarea întreagă, ca reîncercările să nu pornească toate deodată după o cădere.
Dezactivarea automată necesită atât un prag (20 de eșecuri consecutive sau 5 erori de configurare), cât și cel puțin 15 minute consecutive de eșecuri — o repornire scurtă nu poate ucide integrarea, chiar dacă multe livrări s-au adunat în coadă. Un interval mai mare de 15 minute reia numărătoarea. Dashboard-ul arată motivul, cu codul de răspuns și textul erorii, plus un buton Re-enable care resetează contoarele.
| Plan | Webhook-uri per workspace |
|---|---|
| Free | Indisponibil |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoint-urile trebuie să fie https, cu IP public — adresele private și de loopback
sunt respinse, inclusiv la o redirecționare — și să nu depășească două redirecționări
în adâncime.
Se urmează doar redirecționările 307 și 308. 301, 302 și 303 instruiesc
clientul să treacă la GET și să renunțe la corp, așa că o livrare nu le urmează, iar
încercarea se contorizează drept eșuată. Dacă load balancer-ul tău
normalizează URL-ul (adăugând www sau o bară finală), îndreaptă webhook-ul direct
către URL-ul final.
Webhook-urile se gestionează în dashboard. Dashboard-ul acționează un API de
administrare cu domeniu la nivel de workspace, servit pe host-ul aplicației (de exemplu
https://app.tracio.ai/api/v1), iar endpoint-urile de mai jos sunt exact cele pe care
le apelează. Fiecare endpoint de webhook se află sub /workspaces/{wsId}.
Aceasta nu este o suprafață server-la-server. API-ul de administrare acceptă doar JWT-ul sesiunii tale de dashboard, verificat față de rolul tău în workspace (RBAC); o cheie secretă
tracio_sk_…este respinsă aici. Întrucât acea sesiune trăiește în browser și expiră odată cu el, tratează apelurile de mai jos ca pe o descriere a ceea ce face dashboard-ul, nu ca pe o integrare de automatizat. Pentru acces programatic din propriul backend, folosește Data API, care este doar pentru citire.
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": [] }'Secretul de semnare este generat de TRACIO și returnat o singură dată, la creare
(și la rotație), sub signingSecret. Păstrează-l în siguranță — este cheia cu care
verifici semnăturile.
{ "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" }}La citirile ulterioare signingSecret este mascat (null) — este dezvăluit doar de
creare și de rotația secretului.
| Metodă | Cale | Descriere |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Listarea webhook-urilor |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Actualizarea url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Ștergerea unui webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Trimiterea unei livrări de test semnate |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotirea secretului de semnare |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Listarea încercărilor recente de livrare |
Returnează un 2xx cât mai repede posibil și procesează payload-ul asincron, pentru a
evita timeout-urile:
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) }}Folosește acțiunea Test de pe un webhook (sau POST .../webhooks/{webhookId}/test)
pentru a trimite către endpoint-ul tău un payload de exemplu semnat și a
confirma că este accesibil și că verifică semnăturile corect.
Pentru dezvoltare locală, expune-ți serverul printr-un tunel precum ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint