Webhooki dostarczają zdarzenia identyfikacji na Twój serwer w czasie rzeczywistym.
Za każdym razem, gdy odwiedzający zostaje zidentyfikowany, TRACIO wysyła żądanie HTTP
POST na skonfigurowany przez Ciebie adres URL webhooka. Ciało żądania jest
payloadem zdarzenia.
Są też jedynym kanałem dostarczającym późne werdykty — te, w których zachowanie odwiedzającego dowiodło automatyzacji już po załadowaniu strony.
Skonfiguruj je w panelu w sekcji Settings → Webhooks. Webhooki wymagają planu Pro lub wyższego.
| Zdarzenie | Kiedy | Plan |
|---|---|---|
identification | Przy każdej wizycie — fazy primary, late i correction | Wszystkie |
account_takeover | Zachowanie na koncie przestaje pasować do profilu jego właściciela | Business+ |
account_assessment | Stan oceny przejęcia konta: oceniono i bez zastrzeżeń, oceniono i występuje rozbieżność albo nie oceniono | Business+ |
attack_detected | Skok liczby botów na Twojej stronie | Business+ |
reputation_changed | Reputacja powiązanej osoby przeszła do innego przedziału | Business+ |
transaction_state_changed | Zmienił się wynik transakcji wysłanej przez Ciebie do POST /v2/transactions | Business+ |
login_risk | Ocena logowania przez POST /v2/logins/assess zakończyła się wynikiem challenge lub review | Wszystkie |
trusted_device_enrolled | Urządzenie zostało oznaczone jako zaufane dla konta albo jego zaufanie odnowiono | Wszystkie |
trusted_device_revoked | Zaufanie do urządzenia zostało cofnięte — przez API lub z panelu | Wszystkie |
affiliate_threshold_crossed | Rekomendacja dla partnera afiliacyjnego się pogorszyła (none → review, none → hold, review → hold) | Wszystkie |
Nazwy zdarzeń używają podkreśleń, nigdy kropek — nie ma visitor.created ani
session.created.
Webhook subskrybuje konkretne typy; osobna wartość * oznacza „każdy typ, w tym te
dodane później”. Pięć typów stanowi wyjątek: * nie obejmuje account_assessment,
login_risk, trusted_device_enrolled, trusted_device_revoked ani
affiliate_threshold_crossed — każdy z nich trzeba wskazać jawnie.
account_assessment raportuje pokrycie, a nie znalezisko, więc przychodzi mniej
więcej raz na każdą wizytę z linked_id — to inny rząd wolumenu niż w przypadku
pozostałych zdarzeń. Typy dotyczące ryzyka logowania i partnerów afiliacyjnych mają
sens tylko dla integracji, która wywołuje te API. Symbol wieloznaczny ustawiony,
zanim te zdarzenia powstały, nie jest zgodą na ich otrzymywanie.
Nieznany typ jest odrzucany kodem 400 przy tworzeniu lub edycji subskrypcji, więc
literówka nie zostawi Cię z webhookiem, który po cichu nigdy się nie uruchamia.
identificationJedna wizyta generuje do trzech dostarczeń, które współdzielą to samo request_id:
primary — pierwszy werdykt, przy załadowaniu strony.late — wzbogacenie mniej więcej dziewięć sekund później, gdy dotrą wolne kontrole.correction — korekta na podstawie zachowania (wskaźnik, klawiatura, przewijanie).Koreluj je po request_id, a rozróżniaj po phase. Późniejsza faza ma
pierwszeństwo: jeśli primary powiedział human, a correction mówi bot,
poprawna jest ta druga.
Nie polegaj na kolejności dotarcia. Każda faza jest dostarczana niezależnie i ma
własny harmonogram ponawiania — jeśli primary trafił do ponawiania, a late
powiódł się za pierwszym razem, otrzymasz je w odwrotnej kolejności. Pierwszeństwo
ustalaj na podstawie pola phase, a nie czasu odbioru.
To jedyne trzy fazy zdarzenia identification. Dociera do Ciebie jeszcze jedna
wartość: account_takeover i account_assessment niosą phase: "beacon", ponieważ
oba są podnoszone z beacona behawioralnego sesji.
Zwróć uwagę na rozbieżność, którą to tworzy, bo dotyka ona idempotencji. Produkcyjne
dostarczenie identification ma event_id równe dokładnie <request_id>:<phase>,
ale inne dostarczenia łamią tę formułę. account_takeover to <request_id>:ato —
sufiksem jest literał ato, a nie wartość pola phase — a account_assessment to
<request_id>:ato-state-<state>. Dostarczenie testowe wysłane z panelu to
<request_id>:test, podczas gdy phase w jego ciele wciąż pokazuje primary — a
ciało w schemacie 1 w ogóle nie ma pola phase, więc ten sufiks widać wyłącznie w
nagłówku. Używaj event_id wprost jako klucza idempotencji i nigdy nie składaj go z
powrotem z request_id i phase. Dopasowuj wartości, które obsługujesz, a resztę
ignoruj, zamiast odrzucać dostarczenie.
Niektóre zdarzenia nie mają wizyty: attack_detected (poziom workspace),
reputation_changed (poziom osoby), transaction_state_changed (poziom transakcji),
login_risk, trusted_device_enrolled i trusted_device_revoked (poziom konta)
oraz affiliate_threshold_crossed (poziom partnera afiliacyjnego). Nie mają
request_id, visitor_id ani żadnego z bloków browser, geo, bot czy
decision — tych kluczy po prostu nie ma, podobnie jak nagłówka
X-Tracio-Request-Id. account_takeover i account_assessment natomiast powstają z
konkretnej wizyty i niosą pełne ciało identyfikacji dla Twojego planu. Jeśli
parsujesz wszystkie zdarzenia w jednym handlerze, sprawdź event, zanim sięgniesz po
pola wizyty.
Nowe webhooki używają wersji payloadu 3. Istniejące webhooki w wersji 1 lub 2 zachowują wybraną wersję; oba starsze schematy są zamrożone i zostaną usunięte 2027-04-12. Aby przejść na nową wersję, wybierz wersję 3 na karcie webhooka w panelu.
| Wersja | Status |
|---|---|
1 | Zamrożona; usunięcie 2027-04-12 |
2 | Zamrożona; usunięcie 2027-04-12 |
3 | Domyślna dla nowych webhooków; klucze w snake_case |
Wersja 3 niesie te same pola i wartości co wersja 2, z kluczami w snake_case i
polem version: 3 na najwyższym poziomie. Algorytm podpisu i nagłówki się nie
zmieniają (X-Tracio-Payload-Version: 3). Klucze w Twoim własnym słowniku fields
to Twoje dane i nie zmieniają nazw.
| v2 | v3 |
|---|---|
eventId | event_id |
requestId | request_id |
visitorId | visitor_id |
linkedId | linked_id |
userAgent | user_agent |
network.connectionType | network.connection_type |
network.proxyDetected | network.proxy_detected |
network.realIp | network.real_ip |
bot.antidetectScore | bot.antidetect_score |
identification.matchType | identification.match_type |
identification.matchConfidence | identification.match_confidence |
identification.incognitoVisits | identification.incognito_visits |
identification.driftScore | identification.drift_score |
decision.riskScore | decision.risk_score |
decision.suspectScore | decision.suspect_score |
deviceInfo | device_info |
deviceInfo.deviceId | device_info.device_id |
deviceInfo.crossBrowser | device_info.cross_browser |
deviceInfo.linkedBrowsers | device_info.linked_browsers |
screen.colorDepth | screen.color_depth |
screen.pixelRatio | screen.pixel_ratio |
clientHints | client_hints |
clientHints.platformVersion | client_hints.platform_version |
clientHints.deviceName | client_hints.device_name |
osEnvironment | os_environment |
environment.virtualMachine | environment.virtual_machine |
spoofing.spoofedAxes | spoofing.spoofed_axes |
spoofing.anonymousBrowser | spoofing.anonymous_browser |
session.durationSeconds | session.duration_seconds |
velocity.uniqueIps | velocity.unique_ips |
velocity.uniqueLocations | velocity.unique_locations |
accountAlert | account_alert |
accountAlert.accountId | account_alert.account_id |
accountAlert.driftScore | account_alert.drift_score |
accountAssessment | account_assessment |
accountAssessment.accountId | account_assessment.account_id |
accountAssessment.profile.windowsEffective | account_assessment.profile.windows_effective |
accountAssessment.profile.oldestWindowAgeSeconds | account_assessment.profile.oldest_window_age_seconds |
attack.windowMinutes | attack.window_minutes |
attack.recentBots | attack.recent_bots |
attack.recentTotal | attack.recent_total |
personId | person_id |
reputation.overallRisk | reputation.overall_risk |
linkedAccountsCount | linked_accounts_count |
linkedVisitorsCount | linked_visitors_count |
reputationChange | reputation_change |
reputationChange.personId | reputation_change.person_id |
reputationChange.previousBand | reputation_change.previous_band |
reputationChange.visitorIds | reputation_change.visitor_ids |
reputationChange.linkedAccountsCount | reputation_change.linked_accounts_count |
{ "version": 3, "event": "identification", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:primary", // "<request_id>:<phase>" — the idempotency key "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", // visit identifier, shared by all phases "phase": "primary", "visitor_id": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "linked_id": "user-42", // your ?lid=, if you passed one "tag": "checkout", "fields": { "order_ref": "A-1029", "plan": "pro" }, // custom fields that arrived with the visit "timestamp": "2026-07-30T12:00:00Z", "url": "https://shop.example.com/checkout", "ip": "203.0.113.44", "user_agent": "Mozilla/5.0 …", "browser": { "name": "Chrome", "version": "138" }, "os": { "name": "macOS", "version": "15.5" }, "device": "desktop", "gpu": "Intel Iris Plus Graphics 655", // model karty graficznej, znormalizowany; brak, gdy nieznany "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 // ruch HTTP i surowa ścieżka sieciowa wychodzą przez różne sieci }, "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}Wartości zerowe i puste są pomijane. Pola tekstowe i liczbowe o wartości zerowej
(na przykład bot.type dla człowieka albo bot.antidetect_score: 0) nie występują w
JSON-ie — nie czyń ich wymaganymi w swoich schematach i czytaj zagnieżdżone bloki
ostrożnie.
bot.score i decision.risk_score to liczby dziesiętne w skali 0..100 z jedną
cyfrą po przecinku — dokładnie te, które panel i Data API raportują dla tej samej
wizyty. (W zamrożonym schemacie v1 mają inne jednostki: ułamek 0..1 oraz
odpowiednio 0..255.)
bot.type to nazwa rozpoznanego bota, zweryfikowany agent AI (ai_agent:<vendor>)
albo etykieta rodziny. Słownik wartości znajdziesz w sekcji
Typy botów — wewnętrzne nazwy kontroli nie są
ujawniane w żadnym planie. ai_agent:<vendor> oznacza, że żądanie dowiodło
pochodzenia z infrastruktury danego dostawcy — prawidłowym podpisem Web Bot Auth
złożonym kluczem, który dostawca publikuje, albo adresem z publikowanych przez niego
zakresów IP — a nie tylko zadeklarowało je w user agencie. Żądanie, które podaje się
za agenta AI, a któremu przeczy każdy publikowany przez dostawcę dowód, jest
raportowane jako impersonation.
| Pole | Typ | Opis |
|---|---|---|
version | number | Wersja schematu payloadu (3) |
event | string | Typ zdarzenia |
event_id | string | Identyfikator dostarczenia — klucz idempotencji; unikalny dla zdarzenia i wspólny dla wszystkich Twoich webhooków, które je otrzymują, stały między ponowieniami |
request_id | string | Identyfikator wizyty (UUID), wspólny dla wszystkich jej faz |
phase | string | primary, late, correction; account_takeover i account_assessment niosą beacon |
visitor_id | string | Stabilny identyfikator odwiedzającego |
linked_id | string | Powiązany identyfikator przekazany przez klienta |
tag | string | Własny tag przekazany przez klienta |
fields | object | Własne pola dołączone do wizyty w momencie zbierania. Tylko to, co przyszło razem z wizytą — pole dołączone później przez Data API nigdy nie pojawia się w webhooku, w żadnej fazie ani przy żadnym ponowieniu. Klucze i wartości są ciągami znaków. Brak, gdy wizyta nie niosła pól |
timestamp | string | Czas zdarzenia (RFC 3339) |
url | string | Adres URL strony, na której zdarzenie zostało przechwycone |
ip | string | Adres IP klienta |
user_agent | string | Surowy ciąg user-agent klienta |
browser.name / .version | string | Wykryta przeglądarka |
os.name / .version | string | Wykryty system operacyjny |
device | string | Klasa urządzenia (np. desktop, mobile) |
gpu | string | Model karty graficznej zgłaszany przez przeglądarkę (WebGL), znormalizowany do czytelnej nazwy (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer dla rasteryzatorów programowych; brak, gdy nieznany |
geo | object | Geolokalizacja po IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (wartości logiczne) i connection_type |
network.proxy_detected | boolean | Ruch HTTP wizyty i jej surowe ścieżki sieciowe wychodzą przez różne sieci — przed przeglądarką stoi proxy lub VPN. Dwa adresy tego samego dostawcy (NAT operatora, drugie wyjście tego samego VPN) nie liczą się |
bot.result | string | human, bot lub uncertain |
bot.type | string | Nazwa bota, zweryfikowany agent AI lub etykieta rodziny, gdy bot został wykryty |
bot.score | number | Wskaźnik bota (0–100) |
identification.confidence | number | Pewność identyfikacji (0.0–1.0) |
identification.incognito | boolean | Kontekst przeglądania prywatnego / incognito |
decision.action | string | real, fake lub suspicious |
decision.risk_score | number | Zagregowany wskaźnik ryzyka (0–100) |
extensions | array | Rozszerzenia przeglądarki wykryte podczas tej wizyty, jako stabilne identyfikatory maszynowe (nazwy, kategorie i linki do sklepów są w panelu i w Data API). Brak, gdy niczego nie wykryto |
Pro i wyżej — jak odwiedzający zachowuje się w czasie:
{ "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 wyżej — dlaczego werdykt wypadł tak, a nie inaczej:
{ "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, // publiczny adres zaobserwowany na surowej ścieżce sieciowej, czyli adres // za proxy lub VPN; brak, gdy takiego adresu nie zaobserwowano "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // Środowisko pulpitu ZMIERZONE na maszynie z Linuksem ("Mint 22+", // "Ubuntu", "GNOME", "KDE"); User-Agent nie potrafi wyrazić dystrybucji. // Brak, gdy nie ustalono — przy większości wizyt linuksowych i przy każdej // nielinuksowej. "os_environment": "Mint 22+", // Co wizyta twierdziła o sobie wobec tego, co zmierzyły niezależne // kontrole. Obecne tylko wtedy, gdy podmianę faktycznie wykryto; puste // pole `real` oznacza "kontrola milczała", nigdy "potwierdzono". Na osi // `gpu` pole `claimed.gpu` niesie deklarowaną kartę pod tą samą czytelną // nazwą modelu co pole `gpu` najwyższego poziomu. "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"] } }, // Fakty o urządzeniu — to, co przeglądarka odwiedzającego zgłasza o // maszynie, oczyszczone po naszej stronie. screen: rozdzielczość, głębia // koloru i device pixel ratio. locale: własne preferowane języki // przeglądarki i jej strefa czasowa — w odróżnieniu od geo.timezone, // wyprowadzanej z adresu IP; rozbieżność między nimi to częsta oznaka // podrobionej lokalizacji. client_hints: User-Agent Client Hints — // architektura i bitowość procesora, model urządzenia (Android: kod modelu // w `model`, np. "SM-A556B", oraz jego nazwa handlowa z listy urządzeń // Google Play w `device_name`, np. "Samsung Galaxy A55 5G") i dokładna // wersja platformy; zgłaszają je tylko przeglądarki oparte na Chromium. // Każdy blok jest nieobecny, gdy wizyta nie przyniosła takich danych, więc // każdy traktuj jako opcjonalny. "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" }, // Obecne tylko wtedy, gdy karta graficzna sama przedstawiła się jako // wirtualna; hypervisor to słownik zamknięty (vmware, virtualbox, // parallels, qemu, hyperv, bochs, intel-gvt, vgpu). Brak bloku oznacza, że // nie ma takich przesłanek. "environment": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}Słownik kodów przyczyn oraz znaczenie severity opisuje sekcja
Wykrywanie botów.
Pola multikont (Business i wyżej). Gdy dla Twojego workspace włączony jest tryb
osoby, ciało niesie też person_id, blok reputation (score, categories,
overall_risk), linked_accounts_count i linked_visitors_count — czyli informację,
czy odwiedzający jest powiązany z innymi odwiedzającymi i kontami. Warstwa osoby
działa obecnie w trybie obserwacji i dopóki nie zostanie uruchomiona produkcyjnie, tych
pól w ciele nie ma. Nie czyń ich wymaganymi w swoich schematach.
guidance niesie gotowe rekomendacje „co zrobić” dla każdego punktu integracji,
dzięki czemu nie musisz wyprowadzać polityki z surowych wskaźników. Występuje w
każdym zdarzeniu v3 na planie Business+ — allow jest podawane jawnie:
{ "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 }}Każdy scenariusz startuje z allow i po drabinie porusza się wyłącznie w górę:
allow → challenge → review → deny. W obrębie scenariusza wygrywa najostrzejsza
oś, która zadziałała, a overall jest najostrzejszą spośród wszystkich czterech
scenariuszy.
| Rekomendacja | Płatność | Rejestracja | Logowanie | Program partnerski |
|---|---|---|---|---|
allow | Zrealizuj ją | Załóż je | Wpuść | Zalicz konwersję |
challenge | 3-D Secure / potwierdzenie | Captcha, potwierdzenie e-mailem lub telefonem | Wzmocnione 2FA, ponowne uwierzytelnienie | Oznacz jako wątpliwą do czasu pojawienia się aktywności |
review | Zrealizuj, ale skieruj do przeglądu | Załóż z ograniczeniami | Wpuść i podnieś alert | Wstrzymaj wypłatę do weryfikacji |
deny | Nie realizuj transakcji | Odmów założenia konta | Nie wpuszczaj | Nie zaliczaj konwersji |
basis wymienia osie, które zdecydowały o rekomendacji — oś, której propozycja
była łagodniejsza od przyjętej, na liście się nie pojawia. Czytaj ją po kodach,
zamiast zakładać stały zestaw:
Oś (basis) | Co oznacza |
|---|---|
list | Odwiedzający trafił na jedną z list Twojego workspace w trybie enforce. Lista allow nadpisuje każdą oś: każdy scenariusz staje się allow, a basis to dokładnie ["list"] |
rule | Wizyta spełniła jedną z reguł Twojego workspace w trybie enforce; reguła zmienia tylko rekomendację własnego scenariusza (reguła allow łagodzi tylko ten jeden) i jest pomijana, gdy zadziałała już lista |
bot | Detektor botów uznał, że to automatyzacja — odmowa we wszystkich scenariuszach |
risk | Decyzja i ogólny wskaźnik ryzyka wizyty |
behavior | Biometria behawioralna z wystarczającą pewnością wskazuje bota — dodaje tarcia zamiast odmowy |
account_takeover | Zachowanie na tym koncie przestało pasować do profilu jego właściciela — logowanie do challenge, płatność do review |
network | Tor albo VPN wychodzący z centrum danych |
affiliate_pattern | Wzorzec samodzielnie generowanych poleceń — konwersja partnerska jest wstrzymywana jako challenge |
fraud_reputation, multiaccounting | Reputacja i klastrowanie kont osoby stojącej za odwiedzającym; pojawiają się, gdy warstwa osoby zostanie uruchomiona |
version to wersja zestawu reguł — rośnie w miarę ulepszania logiki, więc zapisuj ją
w logach. Wersja 2 wprowadziła oś list, pierwszą, która potrafi złagodzić
rekomendację; wersja 3 przeniosła progi logowania dla account_takeover na skalę
wskaźnika werdyktu przejęcia konta, więc ta sama wizyta może dostać deny tam, gdzie
wersja 2 dawała challenge, i odwrotnie.
Guidance jest addytywne: nowe scenariusze przychodzą jako nowe klucze, nie łamiąc
kontraktu. Późniejsza faza wygrywa, z wyjątkiem rekomendacji częściowej:
dostarczenie policzone na niepełnym zestawie danych wejściowych jest oznaczone jako
"partial": true, a rekomendacja częściowa nie nadpisuje pełnej rekomendacji
otrzymanej wcześniej dla tego samego request_id. W zwykłym dostarczeniu pole
partial w ogóle nie występuje.
Dokładne progi celowo nie są dokumentowane. Rekomendacja, którą da się odtworzyć
wstecz do wskaźnika, przestaje być zabezpieczeniem. Jest dokładnie jeden celowy
wyjątek: account_assessment.verdict.threshold, granica decyzji werdyktu przejęcia
konta opisana poniżej.
account_takeoverTylko Business i Enterprise; na niższych planach dostarczenie nie powstaje, nawet
jeśli webhook subskrybuje to zdarzenie. Ciałem jest pełna koperta identyfikacyjna dla
Twojego planu plus blok account_alert i opisany w następnej sekcji blok
account_assessment, dostarczana najwyżej raz na wizytę:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // alarm podnoszony jest z beacona; tylko event_id mówi "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}Dlaczego type niesie wersję. Konstrukcja stojąca za wskaźnikiem już się
zmieniała: skala i podstawa werdyktu. behavior-drift/2 to pierwsza wersjonowana
konstrukcja. Jeśli skalibrowałeś własne reguły na tej liczbie, traktuj zmianę tej
wartości jako sygnał, by je ponownie sprawdzić.
account_alert.drift_score i identification.drift_score to ta sama liczba
(od 2026-10-03): wygładzony wskaźnik zachowania, na którym oparto werdykt, mierzony
względem pierścienia wcześniejszych zaufanych okien właściciela konta. Przed tą datą
identification.drift_score niósł surową odległość w innej skali — nie mieszaj
wartości sprzed tej daty i po niej. Gdy okno nie zostało ocenione, pole jest nieobecne,
a nie zerowe. Użyj account_assessment.verdict, gdy potrzebujesz liczby razem z
progiem, z którym ją porównano.
W v1 blok nazywa się accountAlert i niesie type, linkedId oraz drift, a bloku
oceny nie ma. W v2 alarm zmienia nazwy dwóch pól — linkedId → accountId oraz
drift → driftScore — i dodaje accountAssessment; v3 używa powyższych nazw w
snake_case. Zaktualizuj swój handler przy zmianie wersji, w przeciwnym razie Twoja
logika przejęcia konta po cichu przestanie widzieć dane.
account_assessmentTylko Business i Enterprise. Wymaga jawnej subskrypcji: * go nie obejmuje.
Bez tego zdarzenia brak alarmu oznacza naraz dwie różne rzeczy: albo oceniliśmy sesję względem profilu właściciela konta i pasowała, albo w ogóle nie mogliśmy jej ocenić — profil właściciela nie jest gotowy, okno było zbyt ubogie albo ruch nie pochodził od człowieka. Oba przypadki wyglądają jak cisza, a cisza czyta się jak bezpieczeństwo. Zwykle nim nie jest: w pełnym dniu produkcyjnym we wrześniu 2026 wstrzymaliśmy się od werdyktu dla 73% okien i 80% kont. Proces zbudowany na zasadzie „brak alarmu znaczy, że wszystko w porządku” opiera się na stanie, którego przeważnie nie mamy.
{ "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 nigdy nie jest nieobecny ani pusty:
state | Znaczenie | Co zrobić |
|---|---|---|
assessed_clear | Oceniono; zachowanie pasuje do właściciela | Nic |
assessed_alert | Oceniono; zachowanie się rozbiega | Dodatkowa weryfikacja — alarm account_takeover niesie ten sam werdykt |
not_assessed | Werdyktu nie wydano | Nie traktuj jako czystego; oprzyj się na własnych sygnałach |
reason (tylko przy not_assessed):
reason | Znaczenie |
|---|---|
profile_not_ready | Wzorzec właściciela nie może obecnie dać werdyktu dla tego konta. Traktuj to jako stan, a nie etap: może się nigdy nie zmienić, więc nie czekaj |
profile_learning | Wzorzec wciąż jest gromadzony |
window_too_sparse | Człowiek był obecny, ale okno zawierało zbyt mało interakcji |
not_computable | Okno i wzorzec nie miały wspólnych osi niosących informację |
bot_traffic | Danych było wystarczająco, ale nie było człowieka do oceny |
Od 2026-10-04 18:00 UTC window_too_sparse i not_computable nie są już wydawane
dla właściciela, którego wzorzec jest gotowy — takie okno jest raportowane jako
profile_not_ready. Oba powody zostają na liście, bo niosą je wcześniejsze
dostarczenia: jeśli Twój handler się na nich rozgałęzia, kieruj profile_not_ready
tą samą ścieżką i nie porównuj częstości tych powodów sprzed tej daty i po niej.
state i reason nie zależą od planu. verdict jest dostępny od Business+, tak jak
drift_score.
Blok verdict występuje tylko przy assessed_clear i assessed_alert. Przy
not_assessed blok jest nieobecny, a nie wypełniony zerami — score równe 0 obok
threshold równego 0 czytałoby się jako „zachowanie pasowało dokładnie”.
| Pole | Znaczenie |
|---|---|
severity | low lub high — celowo dwa przedziały: środkowy wymagałby drugiego progu na tej samej skali |
score | Wartość, na której oparto werdykt |
threshold | Wartość, z którą faktycznie ją porównano |
scale | Jak zbudowany jest wskaźnik: manhattan-normalized to d/(1+d) w znormalizowanej przestrzeni behawioralnej, gdzie 0 oznacza dokładne dopasowanie |
basis | ring — odległość do najbliższego z wcześniejszych zaufanych okien właściciela; centroid — odległość do jego uśrednionego profilu |
windows | Ile okien zagregowano we wskaźnik |
score i threshold są zawsze na tej samej, nazwanej skali, więc para odtwarza
decyzję: alarm uruchamia się dokładnie wtedy, gdy score > threshold.
Jak często przychodzi. Najwyżej jedno dostarczenie na stan na wizytę — w praktyce
jedno na wizytę z linked_id, a nie jedno na okno behawioralne. Stan może się zmienić
w obrębie jednej wizyty — na przykład wzorzec kończy naukę i not_assessed staje się
assessed_clear — wtedy otrzymasz oba, po kolei. To przejście jest możliwe, ale nie
obiecane: profile_not_ready może utrzymywać się przez całe życie konta.
Czego nie obiecuje. Najlepiej chronione są konta aktywnych użytkowników z długimi sesjami i pełnym profilem wzorcowym; najsłabiej — konta rzadko używane, dla których przejęcie jest groźniejsze. To właściwość konstrukcji, a nie próg, który da się wyregulować. Zdarzenie raportuje behawioralną rozbieżność względem wzorca — nie jest obietnicą wykrywania przejęć kont w ogóle.
attack_detectedAlarm o skoku liczby botów przychodzi we wspólnej kopercie v3 z blokiem attack.
Nie ma pól identyfikacji — to zdarzenie na poziomie 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 }}Webhooki w wersji payloadu 1 otrzymują historyczny płaski schemat alarmu — on się nie zmienia.
login_risk, trusted_device_enrolled i trusted_device_revokedTe trzy zdarzenia wynikają z Twoich własnych wywołań API ryzyka logowania
(POST /v2/logins/assess) i jego zaufanych urządzeń. Są dostarczane tylko przy
jawnej subskrypcji po nazwie — * ich nie obejmuje — i tylko do webhooków w
wersji payloadu 2 lub 3; wersja 1 nie ma dla nich ciała.
login_risk jest wysyłane, gdy ocena zwraca challenge lub review — nigdy przy
allow, deny ani not_assessed. Jego blok login_risk to zapisana ocena,
dokładnie w takiej postaci, w jakiej zwróciła ją odpowiedź synchroniczna.
Identyfikator zdarzenia: login-risk:<id>, gdzie <id> to nasz identyfikator
zapisanego rekordu — własne assessment_id weź z ciała.trusted_device_enrolled i trusted_device_revoked są wysyłane przy każdym
dodaniu, odnowieniu i cofnięciu zaufanego urządzenia. Ich blok trusted_device —
user_id, device_ref, enrolled_at, expires_at, revoked_at, revoked_by
(api lub dashboard) — to migawka z chwili zdarzenia; przy
trusted_device_enrolled pola revoked_at i revoked_by mają wartość null.
Identyfikator zdarzenia: trusted-device:<n>.Są wysyłane asynchronicznie, do około pięciu sekund po wywołaniu, i nie niosą
nagłówka X-Tracio-Request-Id — wizytę znajdziesz po login_risk.request_id.
Webhook jest powiadomieniem do audytu i alertów; samą decyzją jest odpowiedź
synchroniczna.
{ "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_crossedDostarczane tylko przy jawnej subskrypcji po nazwie — * go nie obejmuje — i
tylko do webhooków w wersji payloadu 2 lub 3. Co 15 minut TRACIO przelicza wskaźnik
jakości każdego partnera afiliacyjnego, dla którego jest wystarczająco danych, i
wysyła zdarzenie, gdy jego rekomendacja się pogarsza: none → review, none → hold
lub review → hold. Poprawa niczego nie wysyła i pozwala ponownie zgłosić kolejne
pogorszenie. Zdarzenie należy do workspace: trafia do subskrybujących webhooków w
każdym środowisku i nie niesie nagłówka X-Tracio-Request-Id. Identyfikator
zdarzenia: 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 to udziały partnera z ostatnich 90 dni, te same, które pokazuje raport w
panelu: udział rejestracji, które doszły do pierwszego depozytu (reg_to_ftd),
udział graczy wciąż aktywnych 14 dni po rejestracji (active14_share) oraz udziały
konwersji z powielonym urządzeniem lub osobą, z siatki fraudowej, od łowców bonusów,
z centrum danych, VPN lub od bota, oznaczonych jako chargeback lub fraud partnerski
oraz bez odnalezionej wizyty. TRACIO jedynie rekomenduje — samo wstrzymanie i każda
decyzja o wypłacie pozostają w Twojej platformie afiliacyjnej. W wersji payloadu 2 to
samo ciało ma klucze w camelCase: blok to affiliateThreshold, z affiliateId,
previousRecommendation, qualityScore i tak dalej.
transaction_state_changedBusiness i wyżej. Wysyłane, gdy zmienia się wynik transakcji wysłanej przez Ciebie do
POST /v2/transactions: reguły zdecydowały REVIEW lub DECLINE (APPROVE niczego
nie wysyła), analityk skierował transakcję do przeglądu w panelu albo etykieta została
ustawiona, zastąpiona lub usunięta. Użyj go, aby wstrzymać bonus na czas przeglądu
roszczenia i odebrać go lub zwolnić, gdy wynik się ustali, bez odpytywania. *
obejmuje to zdarzenie; jest dostarczane tylko do webhooków w wersji payloadu 2 lub 3.
Webhook powiązany ze środowiskiem otrzymuje je tylko dla tego środowiska.
{ "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 }}| Klucz | Znaczenie |
|---|---|
effective_state | Wynik, według którego należy działać: stan analityka lub etykiety, gdy jest ustawiony, w przeciwnym razie state |
recommendations | Co zrobić z bonusem, wyprowadzone z effective_state |
label, label_source, label_at | Bieżąca etykieta, miejsce jej ustawienia (api lub dashboard) i czas; wszystkie trzy mają wartość null, gdy transakcja nie ma etykiety |
previous_effective_state | effective_state sprzed tej zmiany; null dla zdarzenia wysłanego przy automatycznej decyzji |
Blok pokazuje transakcję taką, jaka jest w chwili wysłania zdarzenia: jeśli dwie
zmiany następują szybko po sobie, oba zdarzenia niosą najnowszy stan, każde z własnym
previous_effective_state i timestamp. Kolejność nie jest gwarantowana —
sortuj po timestamp, a bieżący stan transakcji odczytuj przez API. Identyfikator
zdarzenia: txn-state:<id>:created dla automatycznej decyzji i txn-state:<id>:<n>
dla każdej późniejszej zmiany. Zdarzenie nie mówi, kto ustawił etykietę — tylko skąd
ona pochodzi.
reputation_changedBusiness i wyżej. Wysyłane, gdy reputacja międzyklientowska osoby, którą widziała
Twoja organizacja, przechodzi do innego przedziału (na przykład clean →
suspicious). Wymaga warstwy osoby, która działa obecnie w trybie obserwacji — więc
zdarzenie uruchamia się tylko dla tych workspace'ów, w których włączono rozpoznawanie
tożsamości między urządzeniami. To zdarzenie na poziomie osoby: nie ma request_id
ani bloków wizyty.
{ "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 }}Klucz bloku to reputation_change, a nie reputation: ten drugi jest zajęty przez
reputację w kontekście konkretnej wizyty, a to dwie różne rzeczy. Schemat 1
dostarcza historyczną płaską postać — te same pola na najwyższym poziomie.
Każde dostarczenie zawiera nagłówek X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t to uniksowy znacznik czasu (w sekundach), kiedy żądanie zostało podpisane.v1 to HMAC-SHA256 z "<t>.<rawRequestBody>" zakodowany szesnastkowo, z kluczem
będącym Twoim sekretem webhooka.Znacznik czasu wchodzi w skład podpisywanej treści, co daje ochronę przed powtórzeniem (replay).
Dwie rzeczy trzeba zrobić dobrze, bo inaczej weryfikacja padnie na produkcji:
v1=. Podczas rotacji sekretu
nagłówek niesie dwa podpisy, a parser, który zachowa tylko jeden z nich, będzie
przez całe okno rotacji odrzucał prawidłowe dostarczenia.// 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")})Dostarczenia w schematach 2 i 3 niosą dodatkowo X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Sekret HMAC znają obie strony, więc HMAC dowodzi,
że nadawca zna sekret, ale nie tego, że żądanie pochodzi od TRACIO; dowodzi tego
podpis asymetryczny. Klucze publiczne są publikowane pod adresem
https://api.tracio.ai/.well-known/webhook-keys i indeksowane po kid: odczytaj
pole public_key. Starszy alias publicKey niesie tę samą wartość i zostanie
usunięty 2027-04-12.
Dostarczenia testowe wysyłane z panelu są podpisywane wyłącznie HMAC — prywatny klucz
platformy żyje na węzłach dostarczających i celowo nie jest dostępny dla panelu.
Weryfikator, który twardo wymaga Ed25519, musi przepuszczać dostarczenia testowe
(mają sufiks :test w event_id), inaczej testowanie z panelu zawodzi przy zdrowej
produkcji. Ta sama ostrożność dotyczy kontroli formatu: dostarczenie testowe niesie
request_id w postaci test_<hex> oraz literał test_visitor jako visitor_id,
więc handler walidujący je względem kształtów produkcyjnych odrzuci dostarczenie,
które poza tym jest zbudowane poprawnie.
Po rotacji oba sekrety pozostają ważne przez 24 godziny, a nagłówek niesie oba podpisy, więc możesz zaktualizować konfigurację bez utraty dostarczeń. Akcja Revoke now skraca to okno. Zaktualizuj sekret u siebie w ciągu 24 godzin: gdy okno się zamknie, stary sekret przestanie pasować, a jeśli Twój endpoint odpowiada na nieprawidłowy podpis kodem 4xx, pięć takich odpowiedzi z rzędu wyłączy webhooka.
| Nagłówek | Opis |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — dwa v1= w oknie rotacji |
X-Tracio-Signature-Ed25519 | Podpis platformy, t=<unix>,kid=<id>,v1=<base64> (tylko v2 i v3) |
X-Tracio-Event-Id | Identyfikator dostarczenia — klucz idempotencji |
X-Tracio-Request-Id | Identyfikator wizyty, zgodny z request_id (tylko v2 i v3). Wysyłany tylko dla zdarzeń wizyty — identification, account_takeover i account_assessment |
X-Tracio-Event-Type | Typ zdarzenia (tylko v2 i v3) |
X-Tracio-Delivery-Attempt | Numer próby, począwszy od 1 (tylko v2 i v3) |
X-Tracio-Payload-Version | 3 lub 2 (tylko v2 i v3) |
X-Tracio-Webhook-Id | Identyfikator webhooka, który wygenerował to dostarczenie |
Dostarczenia mogą być ponawiane, a ponowienie niesie ten sam X-Tracio-Event-Id.
Deduplikuj po nim:
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")})Zwróć uwagę, że event_id jest unikalny dla zdarzenia, a nie dla webhooka: jeśli
kilka webhooków w workspace subskrybuje to samo zdarzenie, każdy otrzyma dostarczenie
z tym samym identyfikatorem. Dla identification jest on budowany jako
<request_id>:<phase>, dlatego trzy fazy jednej wizyty deduplikują się niezależnie,
zamiast zlewać się w jedną. Zdarzenia bez wizyty mają własne postacie — patrz opis
każdego zdarzenia powyżej.
Odpowiadaj kodem 2xx — to jedyny znak, że dostarczenie zostało przyjęte.
| Odpowiedź | Co się dzieje |
|---|---|
2xx | Dostarczenie zakończone |
429 Too Many Requests | Nie liczy się jako niepowodzenie i nie zużywa próby; dłuższy Retry-After jest respektowany |
408, 425, 5xx, zerwane połączenie | Ponawiane z rosnącą przerwą |
410 Gone | Endpoint jest traktowany jako usunięty — webhook zostaje natychmiast wyłączony |
Pozostałe 4xx | Ponawiane, ale pięć z rzędu wyłącza webhooka — 400/401/404 nie da się wyleczyć ponawianiem |
Harmonogram ponawiania: 5 s → 30 s → 2 min → 10 min → 30 min → 2 godz. → 6 godz. (8 prób). Pierwsze ponowienia mieszczą się w minucie, więc krótki restart Twojej usługi nie kosztuje Cię powiadomienia. Każda przerwa jest losowana pomiędzy połową a pełną wartością, żeby po awarii ponowienia nie wystrzeliły jedną salwą.
Automatyczne wyłączenie wymaga jednocześnie progu (20 niepowodzeń z rzędu albo 5 błędów konfiguracji) oraz co najmniej 15 minut nieprzerwanych niepowodzeń — krótki restart nie zabije integracji, nawet jeśli w kolejce czekało wiele dostarczeń. Przerwa dłuższa niż 15 minut zeruje licznik. Panel pokazuje przyczynę wraz z kodem odpowiedzi i tekstem błędu oraz przycisk Re-enable, który resetuje liczniki.
| Plan | Webhooków na workspace |
|---|---|
| Free | Niedostępne |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpointy muszą używać https i mieć publiczny adres IP — adresy prywatne i loopback
są odrzucane, również przy przekierowaniu — a przekierowań może być najwyżej dwa.
Podążamy wyłącznie za przekierowaniami 307 i 308. 301, 302 i 303
instruują klienta, by przeszedł na GET i porzucił ciało żądania, więc dostarczenie za
nimi nie podąża, a próba liczy się jako nieudana. Jeśli Twój load balancer normalizuje
adres URL (dodaje www lub końcowy ukośnik), skieruj webhooka od razu na docelowy
adres.
Webhookami zarządza się w panelu. Panel steruje management API w zakresie
workspace, serwowanym na hoście aplikacji (na przykład
https://app.tracio.ai/api/v1), a endpointy poniżej to dokładnie te, które on
wywołuje. Każdy endpoint webhooków znajduje się pod /workspaces/{wsId}.
To nie jest powierzchnia server-to-server. Management API przyjmuje wyłącznie token JWT Twojej sesji panelu, sprawdzany względem Twojej roli w workspace (RBAC); sekretny klucz
tracio_sk_…jest tu odrzucany. Ponieważ ta sesja żyje w przeglądarce i wygasa razem z nią, traktuj poniższe wywołania jako opis tego, co robi panel, a nie jako integrację do zautomatyzowania. Do dostępu programowego z własnego backendu użyj przeznaczonego tylko do odczytu Data API.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{wsId}/webhooks \ -H "Authorization: Bearer <session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'Sekret podpisujący jest generowany przez TRACIO i zwracany jednorazowo przy
utworzeniu (oraz przy rotacji) w polu signingSecret. Przechowuj go bezpiecznie — to
klucz, którym weryfikujesz podpisy.
{ "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" }}Przy kolejnych odczytach signingSecret jest zamaskowany (null) — ujawniają go
tylko utworzenie i rotacja sekretu.
| Metoda | Ścieżka | Opis |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Lista webhooków |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Aktualizacja url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Usunięcie webhooka |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Wysłanie podpisanego dostarczenia testowego |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotacja sekretu podpisującego |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Lista ostatnich prób dostarczenia |
Zwracaj 2xx tak szybko, jak to możliwe, a payload przetwarzaj asynchronicznie, żeby
uniknąć timeoutów:
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) }}Użyj akcji Test na webhooku (lub POST .../webhooks/{webhookId}/test), aby wysłać
podpisany przykładowy payload na swój endpoint i potwierdzić, że jest osiągalny i
poprawnie weryfikuje podpisy.
Na potrzeby lokalnego developmentu wystaw swój serwer przez tunel, na przykład
ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint