Вебхуки доставляють події ідентифікації на ваш сервер у реальному часі. Щоразу, коли
відвідувача ідентифіковано, TRACIO надсилає HTTP-запит POST на налаштовану вами
URL-адресу вебхука. Тіло запиту і є payload події.
Вони ж — єдиний канал, який доставляє пізні вердикти: ті, де поведінка відвідувача довела його автоматизованість уже після завантаження сторінки.
Налаштовуються в дашборді, у розділі Settings → Webhooks. Вебхуки потребують тарифу Pro або вище.
| Подія | Коли | Тариф |
|---|---|---|
identification | На кожен візит — фази primary, late та correction | Усі |
account_takeover | Поведінка під акаунтом більше не збігається з профілем його власника | Business+ |
account_assessment | Стан перевірки акаунта на захоплення: оцінено й чисто, оцінено й є розбіжність або не оцінено | Business+ |
attack_detected | Сплеск ботів на вашому сайті | Business+ |
reputation_changed | Репутація пов’язаної особи перейшла в інший діапазон | Business+ |
transaction_state_changed | Змінився результат транзакції, яку ви надіслали в POST /v2/transactions | Business+ |
login_risk | Оцінка входу через POST /v2/logins/assess завершилася з challenge або review | Усі |
trusted_device_enrolled | Пристрій зробили довіреним для акаунта або продовжили його довіру | Усі |
trusted_device_revoked | Довіру до пристрою відкликано — через API або з дашборда | Усі |
affiliate_threshold_crossed | Рекомендація щодо партнера погіршилася (none → review, none → hold, review → hold) | Усі |
Назви подій пишуться через підкреслення, ніколи через крапку — немає ані
visitor.created, ані session.created.
Вебхук підписується на конкретні типи; окреме значення * означає «усі типи,
включно з тими, що з’являться пізніше». П’ять типів — виняток: * не охоплює
account_assessment, login_risk, trusted_device_enrolled,
trusted_device_revoked і affiliate_threshold_crossed — кожен із них треба
назвати явно. account_assessment повідомляє про покриття, а не про знахідку, тож
надходить приблизно раз на кожен візит із linked_id — це зовсім інший порядок
обсягу, ніж в інших подій. Типи для ризику входу й партнерів мають сенс лише для
інтеграції, яка викликає відповідні API. Шаблон *, заданий до появи цих подій, не
є згодою на їх отримання.
Невідомий тип відхиляється з кодом 400 під час створення або редагування підписки,
тож одруківка не залишить вас із вебхуком, який мовчки ніколи не спрацьовує.
identificationОдин візит породжує до трьох доставок, що мають один і той самий request_id:
primary — початковий вердикт, під час завантаження сторінки.late — збагачення приблизно через дев’ять секунд, коли надійдуть повільні перевірки.correction — коригування на основі поведінки (вказівник, клавіатура, прокручування).Зіставляйте їх за request_id, а розрізняйте за phase. Пізніша фаза має
перевагу: якщо primary сказав human, а correction каже bot, правильна
відповідь — друга.
Не покладайтеся на порядок надходження. Кожна фаза доставляється незалежно й за
власним графіком повторів: якщо primary пішов у повтори, а late пройшов з першої
спроби, ви отримаєте їх у зворотному порядку. Визначайте пріоритет за полем phase,
а не за часом отримання.
Ці три — єдині фази події identification. До вас доходить ще одне значення:
account_takeover і account_assessment несуть phase: "beacon", бо обидві події
піднімає поведінковий beacon сесії.
Зверніть увагу на розбіжність, яку це створює, бо вона впливає на ідемпотентність.
У продовій доставці identification event_id дорівнює рівно <request_id>:<phase>,
але інші доставки цю формулу ламають. account_takeover має вигляд
<request_id>:ato — суфікс тут це літерал ato, а не значення поля phase, — а
account_assessment має вигляд <request_id>:ato-state-<state>. Тестова доставка,
надіслана з дашборда, має вигляд <request_id>:test, тоді як phase у її тілі все
одно показує primary, — а в тілі за схемою 1 поля phase немає взагалі, тож цей
суфікс видно лише в заголовку. Беріть event_id як ключ ідемпотентності напряму й
ніколи не збирайте його заново з request_id і phase. Зіставляйте зі значеннями,
які ви обробляєте, а решту ігноруйте, замість того щоб відхиляти доставку.
Деякі події не прив’язані до візиту: attack_detected (рівень workspace),
reputation_changed (рівень особи), transaction_state_changed (рівень транзакції),
login_risk, trusted_device_enrolled і trusted_device_revoked (рівень акаунта)
та affiliate_threshold_crossed (рівень партнера). У них немає ані request_id, ані
visitor_id, ані блоків browser, geo, bot чи decision — цих ключів просто
немає, як і заголовка X-Tracio-Request-Id. Натомість account_takeover і
account_assessment породжуються конкретним візитом і несуть повне тіло
ідентифікації для вашого тарифу. Якщо ви розбираєте всі події в одному обробнику,
перевіряйте event перш ніж торкатися полів візиту.
Нові вебхуки отримують payload версії 3. Наявні вебхуки на версії 1 чи 2 зберігають обрану версію; обидві старі схеми заморожені й будуть вилучені 2027-04-12. Щоб мігрувати, оберіть версію 3 на картці вебхука в дашборді.
| Версія | Статус |
|---|---|
1 | Заморожена; вилучається 2027-04-12 |
2 | Заморожена; вилучається 2027-04-12 |
3 | Стандартна для нових вебхуків; ключі в snake_case |
Версія 3 несе ті самі поля й значення, що й версія 2, але з ключами в snake_case та
version: 3 на верхньому рівні. Алгоритм підпису й заголовки не змінилися
(X-Tracio-Payload-Version: 3). Ключі всередині вашого власного словника fields —
це ваші дані, їх не перейменовують.
| 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", // модель відеоадаптера, нормалізована; відсутня, коли невідома "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.405, "timezone": "Europe/Berlin" }, "network": { "vpn": false, "proxy": true, "tor": false, "datacenter": true, "connection_type": "DCH", "proxy_detected": true // HTTP-трафік і сирий мережевий шлях виходять через різні мережі }, "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}Нульові та порожні значення опускаються. Рядкові й числові поля з нульовим
значенням (наприклад, bot.type для людини або bot.antidetect_score: 0) у JSON
відсутні — не робіть їх обов’язковими у своїх схемах і читайте вкладені блоки
захисно.
bot.score і decision.risk_score — десяткові числа у шкалі 0..100 з одним
знаком після коми, рівно ті, що дашборд і Data API повідомляють для того самого
візиту. (У замороженій схемі v1 у них інші одиниці: частка 0..1 та 0..255
відповідно.)
bot.type — це ім’я розпізнаного бота, перевірений AI-агент (ai_agent:<vendor>)
або мітка родини. Словник значень див. у розділі
Типи ботів — внутрішні імена перевірок не
розкриваються на жодному тарифі. ai_agent:<vendor> означає, що запит довів своє
походження з інфраструктури цього постачальника — дійсним підписом Web Bot Auth,
зробленим ключем, який постачальник публікує, або адресою з опублікованих ним
діапазонів IP, — а не просто заявив про це в user agent. Запит, що видає себе за
AI-агента, але суперечить кожному доказу, який публікує постачальник, позначається
як impersonation.
| Поле | Тип | Опис |
|---|---|---|
version | number | Версія схеми payload (3) |
event | string | Тип події |
event_id | string | Ідентифікатор доставки — ключ ідемпотентності; унікальний для події, спільний для всіх ваших вебхуків, що її отримують, і незмінний між повторами |
request_id | string | Ідентифікатор візиту (UUID), спільний для всіх його фаз |
phase | string | primary, late, correction; account_takeover і account_assessment несуть beacon |
visitor_id | string | Стабільний ідентифікатор відвідувача |
linked_id | string | Пов’язаний ідентифікатор, переданий клієнтом |
tag | string | Довільний тег, переданий клієнтом |
fields | object | Довільні поля, прикріплені до візиту під час збору. Лише те, що прийшло разом із візитом: поле, додане пізніше через Data API, у вебхуку не з’являється ні в якій фазі чи повторі. Ключі й значення — рядки. Відсутнє, якщо візит полів не ніс |
timestamp | string | Час події (RFC 3339) |
url | string | URL сторінки, де подію було зафіксовано |
ip | string | IP-адреса клієнта |
user_agent | string | Сирий рядок user-agent клієнта |
browser.name / .version | string | Визначений браузер |
os.name / .version | string | Визначена операційна система |
device | string | Клас пристрою (наприклад, desktop, mobile) |
gpu | string | Модель відеоадаптера, як її повідомляє браузер (WebGL), нормалізована до читабельного імені (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer для програмних растеризаторів; відсутня, коли невідома |
geo | object | Геолокація за IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (булеві) та connection_type |
network.proxy_detected | boolean | HTTP-трафік візиту та його сирі мережеві шляхи виходять через різні мережі — перед браузером стоїть проксі або VPN. Дві адреси того самого провайдера (NAT оператора, другий вихід того самого VPN) не рахуються |
bot.result | string | human, bot або uncertain |
bot.type | string | Ім’я бота, перевірений AI-агент або мітка родини, коли бота виявлено |
bot.score | number | Оцінка бота (0–100) |
identification.confidence | number | Упевненість ідентифікації (0.0–1.0) |
identification.incognito | boolean | Приватний режим / інкогніто |
decision.action | string | real, fake або suspicious |
decision.risk_score | number | Сукупна оцінка ризику (0–100) |
extensions | array | Розширення браузера, виявлені в цьому візиті, у вигляді стабільних машинних slug (назви, категорії та посилання на магазини — у дашборді й Data API). Відсутнє, якщо нічого не виявлено |
Pro і вище — як відвідувач поводиться в часі:
{ "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 і вище — чому вердикт вийшов саме таким:
{ "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, // публічна адреса, яку спостерігали на сирому мережевому шляху, тобто // адреса за проксі або VPN; відсутня, коли такої адреси не спостерігали "real_ip": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, "geo": { "isp": "Deutsche Telekom" }, // Середовище робочого столу, ВИМІРЯНЕ на машині з Linux ("Mint 22+", // "Ubuntu", "GNOME", "KDE"); User-Agent виразити дистрибутив не може. // Відсутнє, коли не визначене — у більшості Linux-візитів і в кожному // не-Linux. "os_environment": "Mint 22+", // Що візит заявив про себе проти того, що виміряли незалежні перевірки. // Присутнє лише тоді, коли підміну справді виявлено; порожнє поле `real` // означає "перевірка промовчала", а не "підтверджено". За віссю `gpu` поле // `claimed.gpu` несе заявлений адаптер тим самим читабельним іменем // моделі, що й поле `gpu` верхнього рівня. "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"] } }, // Факти про пристрій — що браузер відвідувача повідомляє про машину, // очищене на нашому боці. screen: роздільна здатність, глибина кольору та // device pixel ratio. locale: власні бажані мови браузера та його часовий // пояс — на відміну від geo.timezone, який виводиться з IP-адреси; // розбіжність між ними — часта ознака підміненого розташування. // client_hints: User-Agent Client Hints — архітектура та розрядність // процесора, модель пристрою (на Android: код моделі в `model`, // наприклад "SM-A556B", і її маркетингова назва зі списку пристроїв // Google Play у `device_name`, наприклад "Samsung Galaxy A55 5G") і точна // версія платформи; повідомляють їх лише браузери на Chromium. Кожен блок // відсутній, коли візит таких даних не приніс, тож вважайте будь-який із // них необов’язковим. "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" }, // Присутнє лише тоді, коли відеоадаптер сам відрекомендувався віртуальним; // hypervisor — закритий словник (vmware, virtualbox, parallels, qemu, // hyperv, bochs, intel-gvt, vgpu). Відсутність блока означає, що таких // свідчень немає. "environment": { "virtual_machine": true, "hypervisor": "vmware" }, "decision": { "suspect_score": 4.1 }, "guidance": { "version": 3, "overall": "review" } // see below}Словник кодів причин і значення severity — у розділі
Виявлення ботів.
Поля мультиакаунтингу (Business і вище). Щойно для вашого workspace увімкнено
режим особи, тіло додатково несе person_id, блок reputation (score,
categories, overall_risk), linked_accounts_count і linked_visitors_count —
чи пов’язаний відвідувач з іншими відвідувачами та акаунтами. Шар особи зараз
працює в режимі спостереження, і доки його не запущено в бойовому режимі, цих полів
у тілі немає. Не робіть їх обов’язковими у своїх схемах.
guidance несе готові рекомендації «що робити» для кожної точки інтеграції, щоб вам
не доводилося виводити політику із сирих оцінок. Він присутній у кожній події v3 на
Business+ — allow вказується явно:
{ "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 }}Кожен сценарій стартує з allow і рухається лише вгору драбиною:
allow → challenge → review → deny. Усередині сценарію перемагає найсуворіша
вісь, що спрацювала, а overall — найсуворіша по всіх чотирьох сценаріях.
| Рекомендація | Платіж | Реєстрація | Вхід | Партнерська програма |
|---|---|---|---|---|
allow | Проводьте | Створюйте | Пропускайте | Зарахуйте конверсію |
challenge | 3-D Secure / підтвердження | Капча, підтвердження поштою або телефоном | Step-up 2FA, повторна автентифікація | Позначте як сумнівну, доки не з’явиться активність |
review | Проведіть, але поставте в чергу на перевірку | Створіть з обмеженнями | Пропустіть, але підніміть алерт | Притримайте виплату до перевірки |
deny | Не проводьте транзакцію | Відмовте у створенні акаунта | Не пропускайте | Не зараховуйте конверсію |
basis перелічує осі, які визначили рекомендацію: вісь, чия пропозиція була
м’якшою за прийняту, до списку не потрапляє. Читайте його за кодами, а не
розраховуйте на фіксований набір:
Вісь (basis) | Що означає |
|---|---|
list | Відвідувач потрапив в один зі списків вашого workspace у режимі enforce. Список allow переважує кожну вісь: кожен сценарій стає allow, а basis дорівнює рівно ["list"] |
rule | Візит підпав під одне з правил вашого workspace у режимі enforce; правило зсуває рекомендацію лише свого сценарію (правило allow пом’якшує тільки його) і пропускається, якщо вже спрацював список |
bot | Детектор ботів дійшов висновку, що це автоматизація, — відмова в кожному сценарії |
risk | Рішення та загальна оцінка ризику візиту |
behavior | Поведінкова біометрія достатньо впевнено каже «бот» — додає тертя, а не відмову |
account_takeover | Поведінка під цим акаунтом більше не збігається з профілем власника — вхід переходить у challenge, платіж — у review |
network | Tor або VPN, що виходить із дата-центру |
affiliate_pattern | Картина самостійно згенерованих рефералів — партнерську конверсію притримують як challenge |
fraud_reputation, multiaccounting | Репутація та кластеризація акаунтів особи, що стоїть за відвідувачем; з’являються, щойно шар особи запрацює в бойовому режимі |
version — версія набору правил, вона підвищується в міру покращення логіки, тож
зберігайте її в логах. Версія 2 додала вісь list — першу, здатну пом’якшити
рекомендацію; версія 3 перенесла пороги входу для account_takeover на шкалу оцінки
вердикту про захоплення акаунта, тож той самий візит може отримати deny там, де
версія 2 давала challenge, і навпаки.
Guidance є адитивним: нові сценарії приходять новими ключами, не ламаючи контракт.
Пізніша фаза перемагає, крім часткових рекомендацій: доставка, порахована на
неповному наборі вхідних даних, позначається "partial": true, і часткова
рекомендація не перекриває повну, отриману раніше для того самого request_id.
У звичайній доставці поле partial відсутнє цілком.
Точні пороги навмисно не документуються. Рекомендація, яку можна зворотною
розробкою звести до оцінки, перестає бути захистом. Свідомий виняток рівно один:
account_assessment.verdict.threshold — межа рішення для вердикту про захоплення
акаунта, описана нижче.
account_takeoverЛише Business та Enterprise; на нижчих тарифах доставка не створюється, навіть якщо
вебхук підписано на цю подію. Тіло — повний конверт ідентифікації для вашого тарифу
плюс блок account_alert і блок account_assessment, описаний у наступному розділі;
доставляється не більше ніж один раз на візит:
{ "version": 3, "event": "account_takeover", "event_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "request_id": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // тривогу піднято з beacon; лише event_id каже "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}Навіщо в type версія. Конструкція, що стоїть за оцінкою, уже змінювалася:
змінювалися шкала й основа вердикту. behavior-drift/2 — перша версіонована
конструкція. Якщо ви калібрували власні правила за цим числом, сприймайте зміну
цього значення як сигнал їх перевірити.
account_alert.drift_score і identification.drift_score — те саме число
(з 2026-10-03): згладжена поведінкова оцінка, на якій ухвалено вердикт, виміряна
відносно кільця минулих довірених вікон власника акаунта. До цієї дати
identification.drift_score ніс сиру відстань в іншій шкалі — не змішуйте значення
по різні боки дати. Коли вікно не оцінювали, поле відсутнє, а не дорівнює нулю.
Беріть account_assessment.verdict, коли вам потрібне число разом із порогом, з
яким його порівнювали.
У v1 блок називається accountAlert і несе type, linkedId та drift, а блоку
оцінки немає. У v2 в тривозі перейменовано два поля — linkedId → accountId та
drift → driftScore — і додано accountAssessment; v3 використовує наведені вище
імена в snake_case. Оновіть свій обробник, коли перемикаєте версію, інакше ваша
логіка щодо захоплення акаунтів мовчки перестане бачити дані.
account_assessmentЛише Business та Enterprise. Потребує явної підписки: * її не охоплює.
Без цієї події відсутність тривоги означає дві різні речі водночас: або ми оцінили сесію відносно профілю власника акаунта і вона збіглася, або не змогли оцінити її взагалі — профіль власника не готовий, вікно було надто розрідженим або трафік ішов не від людини. Обидва випадки виглядають як тиша, а тишу сприймають як безпеку. Зазвичай це не так: за повний продовий день у вересні 2026 року ми утрималися від вердикту для 73% вікон і 80% акаунтів. Процес, побудований на «немає тривоги — отже, все гаразд», спирається на стан, якого здебільшого немає.
{ "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 ніколи не буває відсутнім чи порожнім:
state | Значення | Що робити |
|---|---|---|
assessed_clear | Оцінено; поведінка збігається з власником | Нічого |
assessed_alert | Оцінено; поведінка розходиться | Step-up — тривога account_takeover несе той самий вердикт |
not_assessed | Вердикт не ухвалено | Не вважайте це чистим результатом; спирайтеся на власні сигнали |
reason (лише з not_assessed):
reason | Значення |
|---|---|
profile_not_ready | Еталон власника зараз не може дати вердикт для цього акаунта. Сприймайте це як стан, а не етап: він може не змінитися ніколи, тож не чекайте |
profile_learning | Еталон ще накопичується |
window_too_sparse | Людина була, але у вікні було замало взаємодії |
not_computable | У вікна й еталона не знайшлося спільних інформативних осей |
bot_traffic | Даних було достатньо, але оцінювати не було кого — людини там не було |
З 2026-10-04 18:00 UTC window_too_sparse і not_computable більше не видаються
для власника з готовим еталоном — таке вікно повідомляється як
profile_not_ready. Обидві причини залишаються в списку, бо їх несуть раніші
доставки: якщо ваш обробник розгалужується за ними, спрямовуйте
profile_not_ready тим самим шляхом і не порівнюйте частоту цих причин по різні
боки цієї дати.
state і reason не залежать від тарифу. verdict — для Business+, як і
drift_score.
Блок verdict присутній лише з assessed_clear і assessed_alert. З
not_assessed блок відсутній, а не заповнений нулями: score 0 поруч із
threshold 0 читався б як «поведінка збіглася точно».
| Поле | Значення |
|---|---|
severity | low або high — свідомо два діапазони: середній потребував би другого порога на тій самій шкалі |
score | Значення, на якому ухвалено вердикт |
threshold | Значення, з яким його фактично порівнювали |
scale | Як побудовано оцінку: manhattan-normalized — це d/(1+d) у нормалізованому поведінковому просторі, де 0 означає точний збіг |
basis | ring — відстань до найближчого з минулих довірених вікон власника; centroid — відстань до його усередненого профілю |
windows | Скільки вікон агреговано в оцінку |
score і threshold завжди лежать на одній і тій самій названій шкалі, тож пара
відтворює рішення: тривога спрацьовує саме тоді, коли score > threshold.
Як часто надходить. Не більше однієї доставки на стан за візит — на практиці
одна на візит із linked_id, а не одна на поведінкове вікно. Стан може змінитися
в межах одного візиту — наприклад, еталон завершує навчання і not_assessed
стає assessed_clear, — і тоді ви отримаєте обидві доставки, по черзі. Такий
перехід можливий, але не гарантований: profile_not_ready може зберігатися
впродовж усього життя акаунта.
Чого вона не обіцяє. Найкраще захищені активні користувачі з довгими сесіями та повним еталонним профілем; найгірше — ті, хто заходить рідко, а для них захоплення небезпечніше. Це властивість конструкції, а не поріг, який можна підкрутити. Подія повідомляє про поведінкову розбіжність з еталоном — вона не претендує на виявлення захоплення акаунтів загалом.
attack_detectedТривога про сплеск ботів приходить у спільному конверті v3 з блоком attack. Полів
ідентифікації в ній немає — це подія рівня 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 }}Вебхуки на payload версії 1 отримують історичну пласку схему тривоги — вона не змінюється.
login_risk, trusted_device_enrolled і trusted_device_revokedЦі три події породжують ваші власні виклики API ризику входу
(POST /v2/logins/assess) і його довірених пристроїв. Вони доставляються лише за
явною підпискою за назвою — * їх не охоплює — і лише вебхукам на payload
версії 2 або 3; у версії 1 для них немає тіла.
login_risk надсилається, коли оцінка повертає challenge або review, — ніколи
на allow, deny чи not_assessed. Його блок login_risk — це збережена оцінка,
точно така, якою її віддала синхронна відповідь. Ідентифікатор події:
login-risk:<id>, де <id> — наш ідентифікатор збереженого запису; свій
assessment_id беріть із тіла.trusted_device_enrolled і trusted_device_revoked надсилаються на кожне
внесення, продовження й відкликання довіреного пристрою. Їхній блок
trusted_device — user_id, device_ref, enrolled_at, expires_at,
revoked_at, revoked_by (api або dashboard) — це знімок на момент події; у
trusted_device_enrolled поля revoked_at і revoked_by дорівнюють null.
Ідентифікатор події: trusted-device:<n>.Вони надсилаються асинхронно, приблизно до п’яти секунд після виклику, і не несуть
заголовка X-Tracio-Request-Id — щоб знайти візит, беріть login_risk.request_id.
Вебхук — це сповіщення для аудиту й алертів; саме рішення — це синхронна відповідь.
{ "version": 3, "event": "login_risk", "event_id": "login-risk:4b1f9e2c-6d3a-4f80-9b7e-2a5c8d1e0f37", "timestamp": "2026-10-17T10:00:00Z", "login_risk": { "assessment_id": "snap-1", "user_id": "alice", "state": "assessed", "recommendation": "challenge", "reasons": ["new_device"] // …the rest of the assessment }}{ "version": 3, "event": "trusted_device_revoked", "event_id": "trusted-device:7", "timestamp": "2026-10-17T11:00:00Z", "trusted_device": { "user_id": "alice", "device_ref": "7978151c54e94838", "enrolled_at": "2026-10-17T10:00:00Z", "expires_at": "2026-11-16T10:00:00Z", "revoked_at": "2026-10-17T11:00:00Z", "revoked_by": "dashboard" }}affiliate_threshold_crossedДоставляється лише за явною підпискою за назвою — * її не охоплює — і лише
вебхукам на payload версії 2 або 3. Кожні 15 хвилин TRACIO перераховує оцінку якості
кожного партнера, для якого досить даних, і надсилає подію, коли його рекомендація
погіршується: none → review, none → hold або review → hold. Покращення нічого
не надсилає й дозволяє повідомити про наступне погіршення знову. Подія належить
workspace: вона йде на підписані вебхуки кожного середовища й не несе заголовка
X-Tracio-Request-Id. Ідентифікатор події:
affiliate-threshold:<affiliate_id>:<recommendation>:<unix time>.
{ "version": 3, "event": "affiliate_threshold_crossed", "event_id": "affiliate-threshold:aff_17:review:1792490400", "timestamp": "2026-10-20T10:00:00Z", "affiliate_threshold": { "affiliate_id": "aff_17", "recommendation": "review", // none | review | hold "previous_recommendation": "none", "quality_score": 72, // 0..100, higher is better "hold_days": 14, // the current hold period of the workspace "crossed_at": "2026-10-20T10:00:00Z", "metrics": { "reg_to_ftd": 0.31, "active14_share": 0.42, "dup_device_share": 0.3, "dup_person_share": 0.1, "ring_share": 0.05, "bonus_hunter_share": 0.2, "dc_vpn_bot_share": 0.1, "chargeback_share": 0.03, "affiliate_fraud_share": 0, "no_visit_share": 0.05 } }}metrics — частки партнера за останні 90 днів, ті самі, що показує звіт у дашборді:
частка реєстрацій, які дійшли до першого депозиту (reg_to_ftd), частка гравців,
активних і через 14 днів після реєстрації (active14_share), а також частки
конверсій із дубльованим пристроєм чи особою, з шахрайського кільця, від мисливців
за бонусами, з дата-центру, VPN чи від бота, позначених як chargeback або як
партнерське шахрайство, і без знайденого візиту. TRACIO лише рекомендує — саме
утримання й будь-яке рішення про виплату залишаються у вашій партнерській
платформі. На payload версії 2 те саме тіло має ключі в camelCase: блок
affiliateThreshold з affiliateId, previousRecommendation, qualityScore тощо.
transaction_state_changedBusiness і вище. Надсилається, коли змінюється результат транзакції, яку ви
надіслали в POST /v2/transactions: правила вирішили REVIEW або DECLINE
(APPROVE нічого не надсилає), аналітик відправив транзакцію на перевірку в
дашборді або мітку встановлено, замінено чи знято. Користуйтеся нею, щоб
притримати бонус, поки заявка на перевірці, і утримати чи видати його, коли
результат визначено, — без опитування. * охоплює цю подію; доставляється вона
лише вебхукам на payload версії 2 або 3. Вебхук, прив’язаний до середовища,
отримує її лише для цього середовища.
{ "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 }}| Ключ | Значення |
|---|---|
effective_state | Результат, за яким діяти: стан від аналітика або мітки, якщо його задано, інакше state |
recommendations | Що робити з бонусом; виводиться з effective_state |
label, label_source, label_at | Поточна мітка, де її встановлено (api або dashboard) і коли; усі три дорівнюють null, якщо в транзакції немає мітки |
previous_effective_state | effective_state до цієї зміни; null для події, надісланої на автоматичне рішення |
Блок показує транзакцію такою, якою вона є в момент надсилання події: якщо дві
зміни йдуть швидко одна за одною, обидві події несуть останній стан, кожна зі своїм
previous_effective_state і timestamp. Порядок не гарантується — сортуйте за
timestamp, а поточний стан транзакції читайте через API. Ідентифікатор події:
txn-state:<id>:created для автоматичного рішення і txn-state:<id>:<n> для кожної
наступної зміни. Подія не каже, хто встановив мітку, — лише звідки вона прийшла.
reputation_changedBusiness і вище. Надсилається, коли міжклієнтська репутація особи, яку бачила ваша
організація, переходить в інший діапазон (наприклад, clean → suspicious). Подія
потребує шару особи, який зараз працює в режимі спостереження, — тож вона
спрацьовує лише для тих workspace, де увімкнено зіставлення ідентичності між
пристроями. Це подія рівня особи: у неї немає request_id і блоків візиту.
{ "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 }}Ключ блока — reputation_change, а не reputation: другий зайнятий репутацією в
контексті конкретного візиту, а це різні речі. Схема 1 доставляє історичну пласку
форму — ті самі поля на верхньому рівні.
Кожна доставка містить заголовок X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t — Unix-таймстамп (у секундах), коли запит було підписано.v1 — HMAC-SHA256 від "<t>.<rawRequestBody>" у hex-кодуванні, з ключем — вашим
секретом вебхука.Таймстамп входить до підписуваного вмісту, що дає захист від повторів (replay).
Дві речі, які важливо зробити правильно, інакше перевірка зламається на проді:
v1=. У період ротації секрету
заголовок несе два підписи, і парсер, який залишає лише один із них,
відхилятиме валідні доставки протягом усього вікна ротації.// 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")})Доставки за схемами 2 і 3 додатково несуть X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). HMAC-секрет знають обидві сторони, тож HMAC
доводить, що відправник знає секрет, але не те, що запит походить від TRACIO;
асиметричний підпис — доводить. Публічні ключі публікуються на
https://api.tracio.ai/.well-known/webhook-keys з індексацією за kid: читайте
поле public_key. Застарілий псевдонім publicKey несе те саме значення й буде
вилучений 2027-04-12.
Тестові доставки, надіслані з дашборда, підписуються лише HMAC: приватний ключ
платформи живе на вузлах доставки й навмисно недоступний дашборду. Верифікатор,
який жорстко вимагає Ed25519, зобов’язаний пропускати тестові доставки (у них
суфікс :test в event_id), інакше тестування з дашборда падатиме за цілком
здорового проду. Та сама обережність стосується перевірок формату: у тестовій
доставці request_id має вигляд test_<hex>, а visitor_id — це літерал
test_visitor, тож обробник, який звіряє їх із продовими формами, відхилить цілком
справну доставку.
Після ротації обидва секрети залишаються дійсними 24 години, а заголовок несе обидва підписи, тож ви можете оновити конфігурацію, не втрачаючи доставок. Дія Revoke now обриває це вікно достроково. Оновіть секрет у себе протягом 24 годин: щойно вікно закриється, старий секрет перестане збігатися, а якщо ваша кінцева точка відповідає на невірний підпис кодом 4xx, п’ять таких відповідей поспіль вимкнуть вебхук.
| Заголовок | Опис |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — дві v1= у вікні ротації |
X-Tracio-Signature-Ed25519 | Підпис платформи, t=<unix>,kid=<id>,v1=<base64> (лише v2 і v3) |
X-Tracio-Event-Id | Ідентифікатор доставки — ключ ідемпотентності |
X-Tracio-Request-Id | Ідентифікатор візиту, збігається з request_id (лише v2 і v3). Надсилається лише для подій візиту — identification, account_takeover і account_assessment |
X-Tracio-Event-Type | Тип події (лише v2 і v3) |
X-Tracio-Delivery-Attempt | Номер спроби, починаючи з 1 (лише v2 і v3) |
X-Tracio-Payload-Version | 3 або 2 (лише v2 і v3) |
X-Tracio-Webhook-Id | Ідентифікатор вебхука, який сформував цю доставку |
Доставки можуть повторюватися, і повтор несе той самий X-Tracio-Event-Id.
Дедуплікуйте за ним:
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")})Зверніть увагу: event_id унікальний для події, а не для вебхука: якщо кілька
вебхуків у workspace підписані на ту саму подію, кожен отримає доставку з тим самим
ідентифікатором. Для identification він будується як <request_id>:<phase>, тому
три фази одного візиту дедуплікуються незалежно, а не схлопуються в одну. Події без
візиту мають власні форми — див. опис кожної події вище.
Відповідайте кодом 2xx — це єдина ознака того, що доставку прийнято.
| Відповідь | Що відбувається |
|---|---|
2xx | Доставку завершено |
429 Too Many Requests | Не рахується збоєм і не витрачає спробу; довший Retry-After дотримується |
408, 425, 5xx, розрив з’єднання | Повторюється зі зростаючою паузою |
410 Gone | Кінцева точка вважається видаленою — вебхук вимикається негайно |
Інші 4xx | Повторюється, але п’ять поспіль вимикають вебхук — 400/401/404 повторами не лікуються |
Графік повторів: 5 с → 30 с → 2 хв → 10 хв → 30 хв → 2 год → 6 год (8 спроб). Перші повтори вкладаються у хвилину, тож короткий перезапуск вашого сервісу не коштує вам сповіщення. Кожна пауза випадково розмивається між половиною та повним значенням, щоб після збою повтори не били одним залпом.
Автоматичне вимкнення потребує одночасно і порога (20 збоїв поспіль або 5 помилок конфігурації), і щонайменше 15 хвилин безперервних збоїв — короткий перезапуск не вб’є інтеграцію, навіть якщо у черзі накопичилося багато доставок. Розрив, довший за 15 хвилин, обнуляє лічбу. Дашборд показує причину, разом із кодом відповіді й текстом помилки, та кнопку Re-enable, яка скидає лічильники.
| Тариф | Вебхуків на workspace |
|---|---|
| Free | Недоступно |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Кінцеві точки мають бути https і з публічною IP-адресою — приватні та
loopback-адреси відхиляються, зокрема й на редиректі, — а глибина редиректів не
більша за два.
Йдемо лише за редиректами 307 і 308. 301, 302 та 303 наказують клієнту
перемкнутися на GET і відкинути тіло, тож доставка за ними не йде, а спроба
зараховується як невдала. Якщо ваш балансувальник нормалізує URL (додає www або
завершальну скісну риску), спрямуйте вебхук одразу на фінальну URL-адресу.
Вебхуками керують у дашборді. Дашборд працює з management API з областю дії
workspace, який обслуговується на хості застосунку (наприклад,
https://app.tracio.ai/api/v1), і саме перелічені нижче кінцеві точки він викликає.
Усі кінцеві точки вебхуків розташовані під /workspaces/{wsId}.
Це не поверхня для взаємодії між серверами. Management API приймає лише JWT вашої сесії дашборда, звіряючи його з вашою роллю у workspace (RBAC); секретний ключ виду
tracio_sk_…тут відхиляється. Оскільки ця сесія живе у браузері й спливає разом із ним, сприймайте виклики нижче як опис того, що робить дашборд, а не як інтеграцію для автоматизації. Для програмного доступу з вашого власного бекенду користуйтеся 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": [] }'Секрет підпису генерується TRACIO і повертається один раз під час створення (та
під час ротації) у полі signingSecret. Зберігайте його безпечно — це ключ, яким ви
перевіряєте підписи.
{ "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" }}Під час наступних читань signingSecret замаскований (null) — він розкривається
лише при створенні та при ротації секрету.
| Метод | Шлях | Опис |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Список вебхуків |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Оновити url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Видалити вебхук |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Надіслати підписану тестову доставку |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Ротувати секрет підпису |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Список нещодавніх спроб доставки |
Повертайте 2xx якомога швидше і обробляйте payload асинхронно, щоб уникнути
таймаутів:
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) }}Скористайтеся дією Test на вебхуку (або POST .../webhooks/{webhookId}/test),
щоб надіслати підписаний зразок payload на вашу кінцеву точку і переконатися, що
вона доступна й коректно перевіряє підписи.
Для локальної розробки відкрийте доступ до вашого сервера через тунель, наприклад
ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint