Вебхуки доставляют события идентификации на ваш сервер в реальном времени. Каждый
раз, когда посетитель идентифицирован, 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_agent:<vendor>) или метка семейства. Словарь значений см. в разделе
Типы ботов — внутренние имена проверок не
раскрываются ни на одном тарифе. ai_agent:<vendor> означает, что запрос доказал своё
происхождение из инфраструктуры этого вендора — действительной подписью Web Bot Auth
ключом, который вендор публикует, или адресом из публикуемых вендором диапазонов IP, —
а не просто заявил об этом в user agent. Запрос, который выдаёт себя за ИИ-агента, но
опровергается каждым публикуемым вендором доказательством, помечается как
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 | Имя бота, проверенный ИИ-агент или метка семейства, когда бот обнаружен |
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 | Расширения браузера, обнаруженные в этом визите, в виде стабильных машинных слагов (названия, категории и ссылки на магазины — в дашборде и в 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 / подтверждение | Капча, подтверждение по email или телефону | 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 или от бота, помеченных как чарджбэк или как
партнёрское мошенничество, и без найденного визита. 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}.
Это не server-to-server-поверхность. 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