Вебхуки доставляють події ідентифікації на ваш сервер у реальному часі. Щоразу, коли
відвідувача ідентифіковано, TRACIO надсилає HTTP-запит POST на налаштовану вами
URL-адресу вебхука. Тіло запиту і є payload події.
Вони ж — єдиний канал, який доставляє пізні вердикти: ті, де поведінка відвідувача довела його автоматизованість уже після завантаження сторінки.
Налаштовуються в дашборді, у розділі Settings → Webhooks. Вебхуки потребують тарифу Pro або вище.
| Подія | Коли | Тариф |
|---|---|---|
identification | На кожен візит — фази primary, late та correction | Усі |
account_takeover | Поведінка під акаунтом більше не збігається з профілем його власника | Business+ |
attack_detected | Сплеск ботів на вашому сайті | Business+ |
reputation_changed | Репутація людини, що стоїть за пристроєм, змінилася | Business+ |
Назви подій пишуться через підкреслення, ніколи через крапку — немає ані
visitor.created, ані session.created. reputation_changed потребує шару особи,
тож спрацьовує лише для тих workspace, де увімкнено зіставлення ідентичності між
пристроями.
Вебхук підписується на конкретні типи; окреме значення * означає «усі типи,
включно з тими, що з’являться пізніше». Невідомий тип відхиляється з кодом 400
під час створення або редагування підписки, тож одруківка не залишить вас із
вебхуком, який мовчки ніколи не спрацьовує.
identificationОдин візит породжує до трьох доставок, що мають один і той самий requestId:
primary — початковий вердикт, під час завантаження сторінки.late — збагачення приблизно через дев’ять секунд, коли надійдуть повільні перевірки.correction — коригування на основі поведінки (вказівник, клавіатура, прокручування).Зіставляйте їх за requestId, а розрізняйте за phase. Пізніша фаза має
перевагу: якщо primary сказав human, а correction каже bot, правильна
відповідь — друга.
Не покладайтеся на порядок надходження. Кожна фаза доставляється незалежно й за
власним графіком повторів: якщо primary пішов у повтори, а late пройшов з першої
спроби, ви отримаєте їх у зворотному порядку. Визначайте пріоритет за полем phase,
а не за часом отримання.
Ці три — єдині фази події identification. До вас доходить ще одне значення:
account_takeover несе phase: "beacon", бо тривогу про захоплення акаунта завжди
піднімає поведінковий beacon.
Зверніть увагу на розбіжність, яку це створює, бо вона впливає на ідемпотентність.
У продовій доставці identification eventId дорівнює рівно <requestId>:<phase>,
але дві доставки ламають цю формулу. account_takeover має вигляд <requestId>:ato —
суфікс тут це літерал ato, а не значення поля phase. Тестова доставка, надіслана
з дашборда, має вигляд <requestId>:test, тоді як phase у її тілі за схемою 2 усе
одно показує primary, — а в тілі за схемою 1 поля phase немає взагалі, тож цей
суфікс видно лише в заголовку. Беріть eventId як ключ ідемпотентності напряму й
ніколи не збирайте його заново з requestId і phase. Зіставляйте зі значеннями,
які ви обробляєте, а решту ігноруйте, замість того щоб відхиляти доставку.
attack_detected — подія рівня workspace: у неї немає ані requestId, ані
visitorId, ані блоків browser, geo, bot, decision — цих ключів просто
немає. account_takeover породжується конкретним візитом і несе повне тіло
ідентифікації для вашого тарифу плюс блок accountAlert. Якщо ви розбираєте всі
події в одному обробнику, перевіряйте event перш ніж торкатися полів візиту.
| Версія | Для кого | Як перемкнути |
|---|---|---|
1 | Вебхуки, створені до появи v2 | Залишається для них значенням за замовчуванням |
2 | Нові вебхуки | Перемикач на картці вебхука в дашборді |
Схема v1 заморожена — жодне її поле не змінюється, тож наявні інтеграції продовжують працювати без правок. Усе нове живе у v2, і саме її віддають нові вебхуки.
{ "version": 2, "event": "identification", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:primary", // "<requestId>:<phase>" — the idempotency key "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", // visit identifier, shared by all phases "phase": "primary", "visitorId": "X7fh2Hg9LkMn3pQr5tBvQw3xZa9mK2pL4nR8dT6y", "linkedId": "user-42", // your ?lid=, if you passed one "tag": "checkout", "timestamp": "2026-07-30T12:00:00Z", "url": "https://shop.example.com/checkout", "ip": "203.0.113.44", "userAgent": "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, "connectionType": "DCH", "proxyDetected": true // HTTP-трафік і сирий мережевий шлях виходять через різні мережі }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Нульові та порожні значення опускаються. Рядкові й числові поля з нульовим
значенням (наприклад, bot.type для людини) у JSON відсутні — не робіть їх
обов’язковими у своїх схемах і читайте вкладені блоки захисно.
bot.score і decision.riskScore — десяткові числа у шкалі 0..100 з одним
знаком після коми, рівно ті, що дашборд повідомляє для того самого візиту. (У
замороженій схемі v1 у них інші одиниці: частка 0..1 та 0..255 відповідно.)
bot.type — це або ім’я розпізнаного бота, або мітка родини. Словник значень див.
у розділі Типи ботів — внутрішні імена перевірок не
розкриваються на жодному тарифі.
| Поле | Тип | Опис |
|---|---|---|
version | number | Версія схеми payload (2) |
event | string | Тип події |
eventId | string | Ідентифікатор доставки — ключ ідемпотентності |
requestId | string | Ідентифікатор візиту (UUID), спільний для всіх його фаз |
phase | string | primary, late, correction; account_takeover несе beacon |
visitorId | string | Стабільний ідентифікатор відвідувача |
linkedId | string | Пов’язаний ідентифікатор, переданий клієнтом |
tag | string | Довільний тег, переданий клієнтом |
timestamp | string | Час події (RFC 3339) |
url | string | URL сторінки, де подію було зафіксовано |
ip | string | IP-адреса клієнта |
userAgent | 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 (булеві) та connectionType |
network.proxyDetected | 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.riskScore | number | Сукупна оцінка ризику (0–100) |
Pro і вище — як відвідувач поводиться в часі:
{ "identification": { "matchType": "exact", // exact | fuzzy | new — how the visitor was recognized "matchConfidence": 0.93, "visits": 42, "incognitoVisits": 3 }, // Present when visitor counters are available at event time (usually primary). // A missing block means "no data", not "zeros". "velocity": { "events5m": 7, "uniqueIps": 2, "uniqueLocations": 1 }, "bot": { "antidetectScore": 0 }, // antidetect indicators, 0..100 "session": { "durationSeconds": 95 } // where the visit duration is already known}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 }, "identification": { "driftScore": 0.31 }, // divergence from the account profile "deviceInfo": { "deviceId": "…", // the physical device across browsers on it "crossBrowser": true, "confidence": 0.88, "linkedBrowsers": 3 }, "network": { "isp": "Deutsche Telekom", "asn": 3320, // публічна адреса, яку спостерігали на сирому мережевому шляху, тобто // адреса за проксі або VPN; відсутня, коли такої адреси не спостерігали "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Середовище робочого столу, ВИМІРЯНЕ на машині з Linux ("Mint 22+", // "Ubuntu", "GNOME", "KDE"); User-Agent виразити дистрибутив не може. // Відсутнє, коли не визначене — у більшості Linux-візитів і в кожному // не-Linux. "osEnvironment": "Mint 22+", // Що візит заявив про себе проти того, що виміряли незалежні перевірки. // Присутнє лише тоді, коли підміну справді виявлено; порожнє поле `real` // означає "перевірка промовчала", а не "підтверджено". За віссю `gpu` поле // `claimed.gpu` несе заявлений адаптер тим самим читабельним іменем // моделі, що й поле `gpu` верхнього рівня. "spoofing": { "detected": true, "claimed": { "os": "Windows 10", "browser": "Chrome 139.0" }, "real": { "os": "macOS" }, "spoofedAxes": ["os", "screen"], // os | gpu | screen | network | browser "anonymousBrowser": { "detected": true, "names": ["Linken Sphere"] } }, // Факти про пристрій — що браузер відвідувача повідомляє про машину, // очищене на нашому боці. screen: роздільна здатність, глибина кольору та // device pixel ratio. locale: власні бажані мови браузера та його часовий // пояс — на відміну від geo.timezone, який виводиться з IP-адреси; // розбіжність між ними — часта ознака підміненого розташування. // clientHints: User-Agent Client Hints — архітектура та розрядність // процесора, модель пристрою (на Android: код моделі в `model`, // наприклад "SM-A556B", і її маркетингова назва зі списку пристроїв // Google Play у `deviceName`, наприклад "Samsung Galaxy A55 5G") і точна // версія платформи; // повідомляють їх лише браузери на Chromium. Кожен блок відсутній, коли // візит таких даних не приніс, тож вважайте будь-який із них // необов'язковим. "screen": { "width": 2560, "height": 1600, "colorDepth": 30, "pixelRatio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "clientHints": { "architecture": "arm", "bitness": "64", "platformVersion": "15.5.0" }, // Присутнє лише тоді, коли відеоадаптер сам відрекомендувався віртуальним; // hypervisor — закритий словник (vmware, virtualbox, parallels, qemu, // hyperv, bochs, intel-gvt, vgpu). Відсутність блока означає, що таких // свідчень немає. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Словник кодів причин і значення severity — у розділі
Виявлення ботів.
guidance несе готові рекомендації «що робити» для кожної точки інтеграції, щоб вам
не доводилося виводити політику із сирих оцінок:
{ "guidance": { "version": 1, "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 | Не проводьте транзакцію | Відмовте у створенні акаунта | Не пропускайте | Не зараховуйте конверсію |
version — версія набору правил, вона підвищується в міру покращення логіки.
Guidance є адитивним: нові сценарії приходять новими ключами, не ламаючи контракт.
Пізніша фаза перемагає, крім часткових рекомендацій: доставка, порахована на
неповному наборі вхідних даних, позначається "partial": true, і часткова
рекомендація не перекриває повну, отриману раніше для того самого requestId.
У звичайній доставці поле partial відсутнє цілком.
Точні пороги навмисно не документуються. Рекомендація, яку можна зворотною розробкою звести до оцінки, перестає бути захистом.
account_takeoverЛише Business та Enterprise. Тіло — повний конверт ідентифікації для вашого тарифу
плюс блок accountAlert, доставляється не більше ніж один раз на візит:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // тривогу піднято з beacon; лише eventId каже "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}У v1 цей блок несе type, linkedId і drift; у v2 два поля перейменовано —
linkedId → accountId та drift → driftScore. Оновіть свій обробник, коли
перемикаєте payloadVersion, інакше ваша логіка щодо захоплення акаунтів мовчки
перестане бачити дані.
attack_detected{ "version": 2, "event": "attack_detected", "eventId": "c0a8e1f2-…", "timestamp": "2026-07-30T12:00:00Z", "attack": { "kind": "bot_spike", "severity": "critical", // info | warning | critical "windowMinutes": 15, "recentBots": 4210, "recentTotal": 5100, "expected": 180.5 // the baseline expected over a window this size }}Кожна доставка містить заголовок 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.visitorId}`) console.log(`Bot: ${event.bot?.result}`) // "human" | "bot" | "uncertain"
res.status(200).send("OK")})Доставки за схемою 2 додатково несуть X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). HMAC-секрет знають обидві сторони, тож HMAC
доводить, що відправник знає секрет, але не те, що запит походить від TRACIO;
асиметричний підпис — доводить. Публічні ключі публікуються на
https://api.tracio.ai/.well-known/webhook-keys та індексуються за kid.
Тестові доставки, надіслані з дашборда, підписуються лише HMAC: приватний ключ
платформи живе на вузлах доставки й навмисно недоступний дашборду. Верифікатор,
який жорстко вимагає Ed25519, зобов’язаний пропускати тестові доставки (у них
суфікс :test в eventId), інакше тестування з дашборда падатиме за цілком
здорового проду. Та сама обережність стосується перевірок формату: у тестовій
доставці requestId має вигляд test_<hex>, а visitorId — це літерал
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) |
X-Tracio-Event-Id | Ідентифікатор доставки — ключ ідемпотентності |
X-Tracio-Request-Id | Ідентифікатор візиту (v2, лише події візиту) |
X-Tracio-Event-Type | Тип події (лише v2) |
X-Tracio-Delivery-Attempt | Номер спроби, починаючи з 1 (лише v2) |
X-Tracio-Payload-Version | 2 (лише v2) |
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")})Зверніть увагу: eventId унікальний на подію, а не на вебхук: якщо кілька
вебхуків у workspace підписані на ту саму подію, кожен отримає доставку з тим самим
ідентифікатором. Він будується як <requestId>:<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?.riskScore > 50) { await alertFraudTeam(event) }
if (event.bot?.result === "bot") { await blockVisitor(event.visitorId) }}Скористайтеся дією Test на вебхуку (або POST .../webhooks/{webhookId}/test),
щоб надіслати підписаний зразок payload на вашу кінцеву точку і переконатися, що
вона доступна й коректно перевіряє підписи.
Для локальної розробки відкрийте доступ до вашого сервера через тунель, наприклад
ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint