Webhooki dostarczają zdarzenia identyfikacji na Twój serwer w czasie rzeczywistym.
Za każdym razem, gdy odwiedzający zostaje zidentyfikowany, TRACIO wysyła żądanie HTTP
POST na skonfigurowany przez Ciebie adres URL webhooka. Ciało żądania jest
payloadem zdarzenia.
Są też jedynym kanałem dostarczającym późne werdykty — te, w których zachowanie odwiedzającego dowiodło automatyzacji już po załadowaniu strony.
Skonfiguruj je w panelu w sekcji Settings → Webhooks. Webhooki wymagają planu Pro lub wyższego.
| Zdarzenie | Kiedy | Plan |
|---|---|---|
identification | Przy każdej wizycie — fazy primary, late i correction | Wszystkie |
account_takeover | Zachowanie na koncie przestaje pasować do profilu jego właściciela | Business+ |
attack_detected | Skok liczby botów na Twojej stronie | Business+ |
reputation_changed | Reputacja osoby stojącej za urządzeniem uległa zmianie | Business+ |
Nazwy zdarzeń używają podkreśleń, nigdy kropek — nie ma visitor.created ani
session.created. reputation_changed wymaga warstwy osoby, więc uruchamia się tylko
dla tych workspace'ów, w których włączono rozpoznawanie tożsamości między
urządzeniami.
Webhook subskrybuje konkretne typy; osobna wartość * oznacza „każdy typ, w tym te
dodane później”. Nieznany typ jest odrzucany kodem 400 przy tworzeniu lub edycji
subskrypcji, więc literówka nie zostawi Cię z webhookiem, który po cichu nigdy się nie
uruchamia.
identificationJedna wizyta generuje do trzech dostarczeń, które współdzielą to samo requestId:
primary — pierwszy werdykt, przy załadowaniu strony.late — wzbogacenie mniej więcej dziewięć sekund później, gdy dotrą wolne kontrole.correction — korekta na podstawie zachowania (wskaźnik, klawiatura, przewijanie).Koreluj je po requestId, a rozróżniaj po phase. Późniejsza faza ma
pierwszeństwo: jeśli primary powiedział human, a correction mówi bot,
poprawna jest ta druga.
Nie polegaj na kolejności dotarcia. Każda faza jest dostarczana niezależnie i ma
własny harmonogram ponawiania — jeśli primary trafił do ponawiania, a late
powiódł się za pierwszym razem, otrzymasz je w odwrotnej kolejności. Pierwszeństwo
ustalaj na podstawie pola phase, a nie czasu odbioru.
To jedyne trzy fazy zdarzenia identification. Dociera do Ciebie jeszcze jedna
wartość: account_takeover niesie phase: "beacon", ponieważ alarm o przejęciu konta
podnoszony jest wyłącznie z beacona behawioralnego.
Zwróć uwagę na rozbieżność, którą to tworzy, bo dotyka ona idempotencji. Produkcyjne
dostarczenie identification ma eventId równe dokładnie <requestId>:<phase>, ale
dwa rodzaje dostarczeń łamią tę formułę. account_takeover to <requestId>:ato —
sufiksem jest literał ato, a nie wartość pola phase. Dostarczenie testowe wysłane
z panelu to <requestId>:test, podczas gdy phase w jego ciele w schemacie 2 wciąż
pokazuje primary — a ciało w schemacie 1 w ogóle nie ma pola phase, więc ten sufiks
widać wyłącznie w nagłówku. Używaj eventId wprost jako klucza idempotencji i nigdy
nie składaj go z powrotem z requestId i phase. Dopasowuj wartości, które
obsługujesz, a resztę ignoruj, zamiast odrzucać dostarczenie.
attack_detected to zdarzenie na poziomie workspace: nie ma requestId, nie ma
visitorId ani żadnego z bloków browser, geo, bot czy decision — tych kluczy
po prostu nie ma. account_takeover powstaje z konkretnej wizyty i niesie pełne ciało
identyfikacji dla Twojego planu plus blok accountAlert. Jeśli parsujesz wszystkie
zdarzenia w jednym handlerze, sprawdź event, zanim sięgniesz po pola wizyty.
| Wersja | Dla kogo | Jak przełączyć |
|---|---|---|
1 | Webhooki utworzone, zanim powstała v2 | Pozostaje dla nich wartością domyślną |
2 | Nowe webhooki | Przełącznik na karcie webhooka w panelu |
Schemat v1 jest zamrożony — żadne jego pole się nie zmienia, więc istniejące integracje działają dalej bez poprawek. Wszystko nowe żyje w v2 i to ją emitują nowe webhooki.
{ "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", // model karty graficznej, znormalizowany; brak, gdy nieznany "geo": { "country": "DE", "city": "Berlin", "lat": 52.52, "lon": 13.405, "timezone": "Europe/Berlin" }, "network": { "vpn": false, "proxy": true, "tor": false, "datacenter": true, "connectionType": "DCH", "proxyDetected": true // ruch HTTP i surowa ścieżka sieciowa wychodzą przez różne sieci }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Wartości zerowe i puste są pomijane. Pola tekstowe i liczbowe o wartości zerowej
(na przykład bot.type dla człowieka) nie występują w JSON-ie — nie czyń ich
wymaganymi w swoich schematach i czytaj zagnieżdżone bloki ostrożnie.
bot.score i decision.riskScore to liczby dziesiętne w skali 0..100 z jedną
cyfrą po przecinku — dokładnie te, które panel raportuje dla tej samej wizyty. (W
zamrożonym schemacie v1 mają inne jednostki: ułamek 0..1 oraz odpowiednio 0..255.)
bot.type to albo nazwa rozpoznanego bota, albo etykieta rodziny. Słownik wartości
znajdziesz w sekcji Typy botów — wewnętrzne nazwy
kontroli nie są ujawniane w żadnym planie.
| Pole | Typ | Opis |
|---|---|---|
version | number | Wersja schematu payloadu (2) |
event | string | Typ zdarzenia |
eventId | string | Identyfikator dostarczenia — klucz idempotencji |
requestId | string | Identyfikator wizyty (UUID), wspólny dla wszystkich jej faz |
phase | string | primary, late, correction; account_takeover niesie beacon |
visitorId | string | Stabilny identyfikator odwiedzającego |
linkedId | string | Powiązany identyfikator przekazany przez klienta |
tag | string | Własny tag przekazany przez klienta |
timestamp | string | Czas zdarzenia (RFC 3339) |
url | string | Adres URL strony, na której zdarzenie zostało przechwycone |
ip | string | Adres IP klienta |
userAgent | string | Surowy ciąg user-agent klienta |
browser.name / .version | string | Wykryta przeglądarka |
os.name / .version | string | Wykryty system operacyjny |
device | string | Klasa urządzenia (np. desktop, mobile) |
gpu | string | Model karty graficznej zgłaszany przez przeglądarkę (WebGL), znormalizowany do czytelnej nazwy (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer dla rasteryzatorów programowych; brak, gdy nieznany |
geo | object | Geolokalizacja po IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (wartości logiczne) i connectionType |
network.proxyDetected | boolean | Ruch HTTP wizyty i jej surowe ścieżki sieciowe wychodzą przez różne sieci — przed przeglądarką stoi proxy lub VPN. Dwa adresy tego samego dostawcy (NAT operatora, drugie wyjście tego samego VPN) nie liczą się |
bot.result | string | human, bot lub uncertain |
bot.type | string | Nazwa bota lub etykieta rodziny, gdy bot został wykryty |
bot.score | number | Wskaźnik bota (0–100) |
identification.confidence | number | Pewność identyfikacji (0.0–1.0) |
identification.incognito | boolean | Kontekst przeglądania prywatnego / incognito |
decision.action | string | real, fake lub suspicious |
decision.riskScore | number | Zagregowany wskaźnik ryzyka (0–100) |
Pro i wyżej — jak odwiedzający zachowuje się w czasie:
{ "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 i wyżej — dlaczego werdykt wypadł tak, a nie inaczej:
{ "reasons": [ // at most 8, sorted by importance { "code": "headless_browser", "severity": "high" }, { "code": "privacy_hardening", "severity": "low" } ], // Behavioral biometrics — present only when behavioral scoring ran for the // visit. A missing block means "no data", never "nothing suspicious". "behavior": { "score": 87, "verdict": "human", "confidence": 0.92 }, "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, // publiczny adres zaobserwowany na surowej ścieżce sieciowej, czyli adres // za proxy lub VPN; brak, gdy takiego adresu nie zaobserwowano "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Środowisko pulpitu ZMIERZONE na maszynie z Linuksem ("Mint 22+", // "Ubuntu", "GNOME", "KDE"); User-Agent nie potrafi wyrazić dystrybucji. // Brak, gdy nie ustalono — przy większości wizyt linuksowych i przy każdej // nielinuksowej. "osEnvironment": "Mint 22+", // Co wizyta twierdziła o sobie wobec tego, co zmierzyły niezależne // kontrole. Obecne tylko wtedy, gdy podmianę faktycznie wykryto; puste // pole `real` oznacza "kontrola milczała", nigdy "potwierdzono". Na osi // `gpu` pole `claimed.gpu` niesie deklarowaną kartę pod tą samą czytelną // nazwą modelu co pole `gpu` najwyższego poziomu. "spoofing": { "detected": true, "claimed": { "os": "Windows 10", "browser": "Chrome 139.0" }, "real": { "os": "macOS" }, "spoofedAxes": ["os", "screen"], // os | gpu | screen | network | browser "anonymousBrowser": { "detected": true, "names": ["Linken Sphere"] } }, // Fakty o urządzeniu — to, co przeglądarka odwiedzającego zgłasza o // maszynie, oczyszczone po naszej stronie. screen: rozdzielczość, głębia // koloru i device pixel ratio. locale: własne preferowane języki // przeglądarki i jej strefa czasowa — w odróżnieniu od geo.timezone, // wyprowadzanej z adresu IP; rozbieżność między nimi to częsta oznaka // podrobionej lokalizacji. clientHints: User-Agent Client Hints — // architektura i bitowość procesora, model urządzenia (Android: kod modelu // w `model`, np. "SM-A556B", oraz jego nazwa handlowa z listy urządzeń // Google Play w `deviceName`, np. "Samsung Galaxy A55 5G") i dokładna // wersja platformy; zgłaszają je tylko przeglądarki oparte na Chromium. // Każdy blok jest nieobecny, gdy wizyta nie przyniosła takich danych, więc // każdy traktuj jako opcjonalny. "screen": { "width": 2560, "height": 1600, "colorDepth": 30, "pixelRatio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "clientHints": { "architecture": "arm", "bitness": "64", "platformVersion": "15.5.0" }, // Obecne tylko wtedy, gdy karta graficzna sama przedstawiła się jako // wirtualna; hypervisor to słownik zamknięty (vmware, virtualbox, // parallels, qemu, hyperv, bochs, intel-gvt, vgpu). Brak bloku oznacza, że // nie ma takich przesłanek. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Słownik kodów przyczyn oraz znaczenie severity opisuje sekcja
Wykrywanie botów.
guidance niesie gotowe rekomendacje „co zrobić” dla każdego punktu integracji,
dzięki czemu nie musisz wyprowadzać polityki z surowych wskaźników:
{ "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 }}Każdy scenariusz startuje z allow i po drabinie porusza się wyłącznie w górę:
allow → challenge → review → deny. W obrębie scenariusza wygrywa najostrzejsza
oś, która zadziałała, a overall jest najostrzejszą spośród wszystkich czterech
scenariuszy.
| Rekomendacja | Płatność | Rejestracja | Logowanie | Program partnerski |
|---|---|---|---|---|
allow | Zrealizuj ją | Załóż je | Wpuść | Zalicz konwersję |
challenge | 3-D Secure / potwierdzenie | Captcha, potwierdzenie e-mailem lub telefonem | Wzmocnione 2FA, ponowne uwierzytelnienie | Oznacz jako wątpliwą do czasu pojawienia się aktywności |
review | Zrealizuj, ale skieruj do przeglądu | Załóż z ograniczeniami | Wpuść i podnieś alert | Wstrzymaj wypłatę do weryfikacji |
deny | Nie realizuj transakcji | Odmów założenia konta | Nie wpuszczaj | Nie zaliczaj konwersji |
version to wersja zestawu reguł — rośnie w miarę ulepszania logiki. Guidance jest
addytywne: nowe scenariusze przychodzą jako nowe klucze, nie łamiąc kontraktu.
Późniejsza faza wygrywa, z wyjątkiem rekomendacji częściowej: dostarczenie
policzone na niepełnym zestawie danych wejściowych jest oznaczone jako
"partial": true, a rekomendacja częściowa nie nadpisuje pełnej rekomendacji
otrzymanej wcześniej dla tego samego requestId. W zwykłym dostarczeniu pole
partial w ogóle nie występuje.
Dokładne progi celowo nie są dokumentowane. Rekomendacja, którą da się odtworzyć wstecz do wskaźnika, przestaje być zabezpieczeniem.
account_takeoverTylko Business i Enterprise. Ciałem jest pełna koperta identyfikacyjna dla Twojego
planu plus blok accountAlert, dostarczana najwyżej raz na wizytę:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // alarm podnoszony jest z beacona; tylko eventId mówi "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}W v1 ten blok niesie type, linkedId i drift; w v2 dwa pola zmieniły nazwę —
linkedId → accountId oraz drift → driftScore. Zaktualizuj swój handler, gdy
przełączasz payloadVersion, w przeciwnym razie Twoja logika przejęcia konta po cichu
przestanie widzieć dane.
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 }}Każde dostarczenie zawiera nagłówek X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t to uniksowy znacznik czasu (w sekundach), kiedy żądanie zostało podpisane.v1 to HMAC-SHA256 z "<t>.<rawRequestBody>" zakodowany szesnastkowo, z kluczem
będącym Twoim sekretem webhooka.Znacznik czasu wchodzi w skład podpisywanej treści, co daje ochronę przed powtórzeniem (replay).
Dwie rzeczy trzeba zrobić dobrze, bo inaczej weryfikacja padnie na produkcji:
v1=. Podczas rotacji sekretu
nagłówek niesie dwa podpisy, a parser, który zachowa tylko jeden z nich, będzie
przez całe okno rotacji odrzucał prawidłowe dostarczenia.// Express.js exampleimport express from "express"import crypto from "crypto"
const app = express()
// Capture the raw body so the signature can be verified byte-for-byte.app.use( express.json({ verify: (req, _res, buf) => { ;(req as any).rawBody = buf }, }),)
function verifySignature(rawBody: Buffer, header: string, secret: string): boolean { if (!header) return false
const parts = header.split(",").map((p) => p.trim()) const ts = parts.find((p) => p.startsWith("t="))?.slice(2) if (!ts) return false
// Replay protection: reject timestamps more than five minutes old. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
// Sign the raw bytes: the "<t>." prefix plus the raw request body. const signed = Buffer.concat([Buffer.from(`${ts}.`, "utf8"), rawBody]) const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex") const exp = Buffer.from(expected, "hex")
// During a rotation window the header carries several v1= — any may match. return parts.some((p) => { if (!p.startsWith("v1=")) return false const got = Buffer.from(p.slice(3), "hex") // Compare lengths BEFORE timingSafeEqual: it throws on differing lengths, // and one junk header would turn the handler into a 500. return got.length === exp.length && crypto.timingSafeEqual(got, exp) })}
app.post("/webhook/tracio", (req, res) => { const header = req.headers["x-tracio-signature"] as string if (!verifySignature((req as any).rawBody, header, WEBHOOK_SECRET)) { return res.status(401).json({ error: "Invalid signature" }) }
const event = req.body console.log(`Visitor: ${event.visitorId}`) console.log(`Bot: ${event.bot?.result}`) // "human" | "bot" | "uncertain"
res.status(200).send("OK")})Dostarczenia w schemacie 2 niosą dodatkowo X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Sekret HMAC znają obie strony, więc HMAC dowodzi,
że nadawca zna sekret, ale nie tego, że żądanie pochodzi od TRACIO; dowodzi tego
podpis asymetryczny. Klucze publiczne są publikowane pod adresem
https://api.tracio.ai/.well-known/webhook-keys i indeksowane po kid.
Dostarczenia testowe wysyłane z panelu są podpisywane wyłącznie HMAC — prywatny klucz
platformy żyje na węzłach dostarczających i celowo nie jest dostępny dla panelu.
Weryfikator, który twardo wymaga Ed25519, musi przepuszczać dostarczenia testowe
(mają sufiks :test w eventId), inaczej testowanie z panelu zawodzi przy w pełni
zdrowej produkcji. Ta sama ostrożność dotyczy kontroli formatu: dostarczenie testowe
niesie requestId w postaci test_<hex> oraz literał test_visitor jako visitorId,
więc handler walidujący je względem kształtów produkcyjnych odrzuci dostarczenie, które
poza tym jest zbudowane poprawnie.
Po rotacji oba sekrety pozostają ważne przez 24 godziny, a nagłówek niesie oba podpisy, więc możesz zaktualizować konfigurację bez utraty dostarczeń. Akcja Revoke now skraca to okno. Zaktualizuj sekret u siebie w ciągu 24 godzin: gdy okno się zamknie, stary sekret przestanie pasować, a jeśli Twój endpoint odpowiada na nieprawidłowy podpis kodem 4xx, pięć takich odpowiedzi z rzędu wyłączy webhooka.
| Nagłówek | Opis |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — dwa v1= w oknie rotacji |
X-Tracio-Signature-Ed25519 | Podpis platformy, t=<unix>,kid=<id>,v1=<base64> (tylko v2) |
X-Tracio-Event-Id | Identyfikator dostarczenia — klucz idempotencji |
X-Tracio-Request-Id | Identyfikator wizyty (v2, tylko zdarzenia wizyty) |
X-Tracio-Event-Type | Typ zdarzenia (tylko v2) |
X-Tracio-Delivery-Attempt | Numer próby, począwszy od 1 (tylko v2) |
X-Tracio-Payload-Version | 2 (tylko v2) |
X-Tracio-Webhook-Id | Identyfikator webhooka, który wygenerował to dostarczenie |
Dostarczenia mogą być ponawiane, a ponowienie niesie ten sam X-Tracio-Event-Id.
Deduplikuj po nim:
app.post("/webhook/tracio", async (req, res) => { const eventId = req.headers["x-tracio-event-id"] as string
const existing = await db.webhooks.findOne({ eventId }) if (existing) return res.status(200).send("Already processed")
await db.webhooks.insert({ eventId, processedAt: new Date() }) await processWebhookEvent(req.body)
res.status(200).send("OK")})Zwróć uwagę, że eventId jest unikalny dla zdarzenia, a nie dla webhooka: jeśli
kilka webhooków w workspace subskrybuje to samo zdarzenie, każdy otrzyma dostarczenie
z tym samym identyfikatorem. Jest on budowany jako <requestId>:<phase>, dlatego trzy
fazy jednej wizyty deduplikują się niezależnie, zamiast zlewać się w jedną.
Odpowiadaj kodem 2xx — to jedyny znak, że dostarczenie zostało przyjęte.
| Odpowiedź | Co się dzieje |
|---|---|
2xx | Dostarczenie zakończone |
429 Too Many Requests | Nie liczy się jako niepowodzenie i nie zużywa próby; dłuższy Retry-After jest respektowany |
408, 425, 5xx, zerwane połączenie | Ponawiane z rosnącą przerwą |
410 Gone | Endpoint jest traktowany jako usunięty — webhook zostaje natychmiast wyłączony |
Pozostałe 4xx | Ponawiane, ale pięć z rzędu wyłącza webhooka — 400/401/404 nie da się wyleczyć ponawianiem |
Harmonogram ponawiania: 5 s → 30 s → 2 min → 10 min → 30 min → 2 godz. → 6 godz. (8 prób). Pierwsze ponowienia mieszczą się w minucie, więc krótki restart Twojej usługi nie kosztuje Cię powiadomienia. Każda przerwa jest losowana pomiędzy połową a pełną wartością, żeby po awarii ponowienia nie wystrzeliły jedną salwą.
Automatyczne wyłączenie wymaga jednocześnie progu (20 niepowodzeń z rzędu albo 5 błędów konfiguracji) oraz co najmniej 15 minut nieprzerwanych niepowodzeń — krótki restart nie zabije integracji, nawet jeśli w kolejce czekało wiele dostarczeń. Przerwa dłuższa niż 15 minut zeruje licznik. Panel pokazuje przyczynę wraz z kodem odpowiedzi i tekstem błędu oraz przycisk Re-enable, który resetuje liczniki.
| Plan | Webhooków na workspace |
|---|---|
| Free | Niedostępne |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpointy muszą używać https i mieć publiczny adres IP — adresy prywatne i loopback
są odrzucane, również przy przekierowaniu — a przekierowań może być najwyżej dwa.
Podążamy wyłącznie za przekierowaniami 307 i 308. 301, 302 i 303
instruują klienta, by przeszedł na GET i porzucił ciało żądania, więc dostarczenie za
nimi nie podąża, a próba liczy się jako nieudana. Jeśli Twój load balancer normalizuje
adres URL (dodaje www lub końcowy ukośnik), skieruj webhooka od razu na docelowy
adres.
Webhookami zarządza się w panelu. Panel steruje management API w zakresie
workspace, serwowanym na hoście aplikacji (na przykład
https://app.tracio.ai/api/v1), a endpointy poniżej to dokładnie te, które on
wywołuje. Każdy endpoint webhooków znajduje się pod /workspaces/{wsId}.
To nie jest powierzchnia server-to-server. Management API przyjmuje wyłącznie token JWT Twojej sesji panelu, sprawdzany względem Twojej roli w workspace (RBAC); sekretny klucz
tracio_sk_…jest tu odrzucany. Ponieważ ta sesja żyje w przeglądarce i wygasa razem z nią, traktuj poniższe wywołania jako opis tego, co robi panel, a nie jako integrację do zautomatyzowania. Do dostępu programowego z własnego backendu użyj przeznaczonego tylko do odczytu Data API.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{wsId}/webhooks \ -H "Authorization: Bearer <session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'Sekret podpisujący jest generowany przez TRACIO i zwracany jednorazowo przy
utworzeniu (oraz przy rotacji) w polu signingSecret. Przechowuj go bezpiecznie — to
klucz, którym weryfikujesz podpisy.
{ "ok": true, "data": { "id": "b3d4f8a1-2c67-4e9b-8f05-7a1d3c9e2b48", "workspaceEnvironmentId": "b201f2ba-…", "url": "https://your-server.com/webhook/tracio", "events": [], "signingSecret": "f3a9…<hex>", "status": "active", "successRate": 100, "createdAt": "2026-07-30T12:00:00Z" }}Przy kolejnych odczytach signingSecret jest zamaskowany (null) — ujawniają go
tylko utworzenie i rotacja sekretu.
| Metoda | Ścieżka | Opis |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Lista webhooków |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Aktualizacja url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Usunięcie webhooka |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Wysłanie podpisanego dostarczenia testowego |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotacja sekretu podpisującego |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Lista ostatnich prób dostarczenia |
Zwracaj 2xx tak szybko, jak to możliwe, a payload przetwarzaj asynchronicznie, żeby
uniknąć timeoutów:
app.post("/webhook/tracio", async (req, res) => { res.status(200).send("OK") processWebhookEvent(req.body).catch(console.error)})
async function processWebhookEvent(event: WebhookPayload) { await db.events.insert(event)
if (event.decision?.riskScore > 50) { await alertFraudTeam(event) }
if (event.bot?.result === "bot") { await blockVisitor(event.visitorId) }}Użyj akcji Test na webhooku (lub POST .../webhooks/{webhookId}/test), aby wysłać
podpisany przykładowy payload na swój endpoint i potwierdzić, że jest osiągalny i
poprawnie weryfikuje podpisy.
Na potrzeby lokalnego developmentu wystaw swój serwer przez tunel, na przykład
ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint