Webhook-urile livrează evenimentele de identificare pe serverul dumneavoastră în timp
real. De fiecare dată când un vizitator este identificat, TRACIO trimite o cerere HTTP
POST către URL-ul de webhook configurat. Corpul cererii este payload-ul
evenimentului.
Ele sunt totodată singurul canal care livrează verdictele târzii — acelea în care comportamentul unui vizitator a dovedit că era automatizat după ce pagina se încărcase deja.
Le configurați în dashboard, la Settings → Webhooks. Webhook-urile necesită planul Pro sau mai mare.
| Eveniment | Când | Plan |
|---|---|---|
identification | La fiecare vizită — fazele primary, late și correction | Toate |
account_takeover | Comportamentul dintr-un cont nu mai corespunde profilului titularului | Business+ |
attack_detected | Un vârf de boți pe site-ul dumneavoastră | Business+ |
reputation_changed | Reputația persoanei din spatele unui dispozitiv s-a schimbat | Business+ |
Numele evenimentelor folosesc underscore, niciodată puncte — nu există
visitor.created sau session.created. reputation_changed are nevoie de stratul de
persoană, așa că se declanșează doar pentru workspace-urile în care rezolvarea
identității între dispozitive este activată.
Un webhook se abonează la tipuri anume; valoarea separată * înseamnă „orice tip,
inclusiv cele adăugate ulterior”. Un tip necunoscut este respins cu 400 la crearea
sau editarea unui abonament, așa că o greșeală de tastare nu vă poate lăsa cu un
webhook care nu se declanșează niciodată, în tăcere.
identificationO singură vizită produce până la trei livrări care împart același requestId:
primary — verdictul inițial, la încărcarea paginii.late — îmbogățire după aproximativ nouă secunde, odată ce verificările lente au sosit.correction — o corecție bazată pe comportament (cursor, tastatură, derulare).Corelați-le după requestId și deosebiți-le după phase. Faza ulterioară are
prioritate: dacă primary a spus human, iar correction spune bot, a doua este
răspunsul corect.
Nu vă bazați pe ordinea sosirii. Fiecare fază este livrată independent și cu
propriul program de reîncercări — dacă primary a intrat în reîncercare în timp ce
late a reușit din prima, le veți primi în ordine inversă. Stabiliți prioritatea din
câmpul phase, nu din momentul recepției.
Acestea trei sunt singurele faze ale unui eveniment identification. O singură altă
valoare ajunge la dumneavoastră: account_takeover poartă phase: "beacon", pentru că
o alertă de preluare a contului este ridicată numai dintr-un beacon comportamental.
Observați nepotrivirea pe care o creează acest lucru, fiindcă afectează idempotența. O
livrare identification din producție are eventId exact <requestId>:<phase>, dar
două livrări încalcă formula. Un account_takeover este <requestId>:ato — sufixul
este literalul ato, nu valoarea câmpului phase. O livrare de test trimisă din
dashboard este <requestId>:test, în timp ce phase din corpul ei în schema 2 arată
tot primary — iar un corp în schema 1 nu are deloc câmpul phase, așa că antetul
este singurul loc în care apare acel sufix. Folosiți eventId direct drept cheie de
idempotență și nu îl reconstruiți niciodată din requestId și phase. Potriviți-vă pe
valorile pe care le tratați și ignorați-le pe celelalte, în loc să respingeți livrarea.
attack_detected este un eveniment la nivel de workspace: nu are requestId, nici
visitorId și niciunul dintre blocurile browser, geo, bot sau decision —
acele chei pur și simplu lipsesc. account_takeover este produs de o vizită anume și
poartă corpul de identificare complet aferent planului dumneavoastră, plus un bloc
accountAlert. Dacă parsați toate evenimentele într-un singur handler, verificați
event înainte de a atinge câmpuri de vizită.
| Versiune | Pentru cine | Cum se comută |
|---|---|---|
1 | Webhook-uri create înainte să existe v2 | Rămâne valoarea implicită pentru ele |
2 | Webhook-uri noi | Comutatorul de pe cardul webhook-ului, în dashboard |
Schema v1 este înghețată — niciunul dintre câmpurile ei nu se schimbă, așa că integrările existente continuă să funcționeze fără modificări. Tot ce este nou trăiește în v2, care este ceea ce emit webhook-urile noi.
{ "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", // modelul plăcii video, normalizat; absent când este necunoscut "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 // traficul HTTP și traseul de rețea brut ies prin rețele diferite }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Valorile zero și cele goale sunt omise. Câmpurile de tip șir și numerice cu
valoare zero (bot.type pentru un om, de exemplu) lipsesc din JSON — nu le faceți
obligatorii în schemele dumneavoastră și citiți blocurile imbricate defensiv.
bot.score și decision.riskScore sunt zecimale pe scara 0..100, cu o cifră
după virgulă — exact numerele pe care dashboard-ul le raportează pentru aceeași
vizită. (În schema înghețată v1 folosesc alte unități: o fracție 0..1, respectiv
0..255.)
bot.type este fie numele unui bot recunoscut, fie o etichetă de familie. Vedeți
Tipuri de bot pentru vocabular — numele interne ale
verificărilor nu sunt expuse niciodată, la niciun plan.
| Câmp | Tip | Descriere |
|---|---|---|
version | number | Versiunea schemei de payload (2) |
event | string | Tipul evenimentului |
eventId | string | Identificatorul livrării — cheia de idempotență |
requestId | string | Identificatorul vizitei (UUID), comun tuturor fazelor ei |
phase | string | primary, late, correction; account_takeover poartă beacon |
visitorId | string | Identificator stabil de vizitator |
linkedId | string | Identificator asociat, furnizat de client |
tag | string | Etichetă personalizată, furnizată de client |
timestamp | string | Ora evenimentului (RFC 3339) |
url | string | URL-ul paginii unde a fost captat evenimentul |
ip | string | Adresa IP a clientului |
userAgent | string | Șirul user-agent brut al clientului |
browser.name / .version | string | Browserul detectat |
os.name / .version | string | Sistemul de operare detectat |
device | string | Clasa dispozitivului (de ex. desktop, mobile) |
gpu | string | Modelul plăcii video așa cum îl raportează browserul (WebGL), normalizat la un nume lizibil (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer pentru rasterizatoarele software; absent când este necunoscut |
geo | object | Geolocalizare după IP: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (booleeni) și connectionType |
network.proxyDetected | boolean | Traficul HTTP al vizitei și traseele ei de rețea brute ies prin rețele diferite — un proxy sau un VPN în fața browserului. Două adrese ale aceluiași furnizor (NAT de operator, o a doua ieșire a aceluiași VPN) nu se pun la socoteală |
bot.result | string | human, bot sau uncertain |
bot.type | string | Numele botului sau eticheta de familie când este detectat un bot |
bot.score | number | Scor de bot (0–100) |
identification.confidence | number | Încrederea identificării (0.0–1.0) |
identification.incognito | boolean | Context de navigare privată/incognito |
decision.action | string | real, fake sau suspicious |
decision.riskScore | number | Scor de risc agregat (0–100) |
Pro și peste — cum se comportă vizitatorul în timp:
{ "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 peste — de ce verdictul a ieșit așa cum a ieșit:
{ "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, // adresa publică observată pe traseul de rețea brut, adică adresa din // spatele proxy-ului sau al VPN-ului; absentă când nu a fost observată // nicio astfel de adresă "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Mediul desktop MĂSURAT pe o mașină Linux ("Mint 22+", "Ubuntu", "GNOME", // "KDE"); un User-Agent nu poate exprima o distribuție. Absent când nu // este determinat — la majoritatea vizitelor Linux și la fiecare vizită // care nu e Linux. "osEnvironment": "Mint 22+", // Ce a pretins vizita despre sine față de ce au măsurat verificări // independente. Prezent doar când o falsificare chiar a fost detectată; un // câmp `real` gol înseamnă "verificarea a rămas tăcută", niciodată // "confirmat". Pe axa `gpu`, `claimed.gpu` poartă placa pretinsă sub // același nume de model lizibil ca și câmpul `gpu` de nivel superior. "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"] } }, // Fapte despre dispozitiv — ce raportează browserul vizitatorului despre // mașină, curățat de partea noastră. screen: rezoluția, adâncimea de // culoare și device pixel ratio. locale: limbile preferate și fusul orar // ale browserului însuși — spre deosebire de geo.timezone, care este // derivat din adresa IP; o nepotrivire între cele două este un semn // frecvent al unei locații falsificate. clientHints: User-Agent Client // Hints — arhitectura și bitness-ul procesorului, modelul dispozitivului // (Android: codul de model din `model`, de ex. "SM-A556B", și numele său // comercial din lista de dispozitive Google Play din `deviceName`, de ex. // "Samsung Galaxy A55 5G") și versiunea exactă a platformei; le raportează doar // browserele bazate pe Chromium. Fiecare bloc lipsește când vizita nu a // purtat astfel de date, așa că tratați-l pe fiecare drept opțional. "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" }, // Prezent doar când placa video s-a declarat ea însăși virtuală; // hypervisor este un dicționar închis (vmware, virtualbox, parallels, // qemu, hyperv, bochs, intel-gvt, vgpu). Un bloc lipsă înseamnă că nu // există un astfel de indiciu. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Vedeți Detectarea boților pentru vocabularul
codurilor de motiv și pentru ce înseamnă severity.
guidance poartă recomandări gata făcute de tipul „ce să faci” pentru fiecare punct
de integrare, astfel încât să nu fiți nevoit să deduceți o politică din scoruri brute:
{ "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 }}Fiecare scenariu pornește de la allow și se poate deplasa doar în sus pe scară:
allow → challenge → review → deny. În cadrul unui scenariu câștigă axa cea mai
strictă care se declanșează, iar overall este cea mai strictă dintre cele patru
scenarii.
| Recomandare | Plată | Înregistrare | Autentificare | Afiliere |
|---|---|---|---|---|
allow | Procesați-o | Creați-l | Lăsați-l să intre | Creditați conversia |
challenge | 3-D Secure / confirmare | Captcha, confirmare pe e-mail sau telefon | 2FA suplimentar, reautentificare | Marcați-o ca îndoielnică până apare activitate |
review | Procesați, dar puneți-o la coadă pentru revizuire | Creați-l cu restricții | Lăsați-l să intre și ridicați o alertă | Rețineți plata până la revizuire |
deny | Nu procesați tranzacția | Refuzați crearea contului | Nu îl lăsați să intre | Nu creditați conversia |
version este versiunea setului de reguli — este incrementată pe măsură ce logica se
îmbunătățește. Guidance este aditiv: scenariile noi apar ca chei noi, fără a strica
contractul. Câștigă faza ulterioară, cu excepția recomandărilor parțiale: o livrare
calculată pe un set incomplet de intrări este marcată cu "partial": true, iar o
recomandare parțială nu înlocuiește o recomandare completă primită mai devreme
pentru același requestId. Într-o livrare obișnuită, câmpul partial lipsește cu
totul.
Pragurile exacte nu sunt documentate în mod deliberat. O recomandare care poate fi supusă ingineriei inverse până la un scor încetează să mai fie o apărare.
account_takeoverDoar Business și Enterprise. Corpul este plicul de identificare complet aferent
planului dumneavoastră plus un bloc accountAlert, livrat cel mult o dată per vizită:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // alerta este ridicată dintr-un beacon; doar eventId spune "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}În v1 acest bloc poartă type, linkedId și drift; în v2 două câmpuri sunt
redenumite — linkedId → accountId și drift → driftScore. Actualizați-vă
handler-ul când comutați payloadVersion, altfel logica dumneavoastră de preluare a
conturilor va înceta în tăcere să mai vadă datele.
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 }}Fiecare livrare include un antet X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t este marca de timp Unix (în secunde) la care a fost semnată cererea.v1 este HMAC-SHA256 codificat hexazecimal al "<t>.<rawRequestBody>", cu secretul
dumneavoastră de webhook drept cheie.Marca de timp face parte din conținutul semnat, ceea ce oferă protecție împotriva reluării.
Două lucruri de făcut corect, altfel verificarea eșuează în producție:
v1=. În timpul unei rotații de
secret, antetul poartă două semnături, iar un parser care păstrează doar una
dintre ele va respinge livrări valide pe toată durata ferestrei de rotație.// 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")})Livrările cu schema 2 poartă suplimentar X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Ambele părți cunosc secretul HMAC, așa că HMAC-ul
dovedește că expeditorul cunoaște secretul, dar nu că cererea provine de la TRACIO;
semnătura asimetrică o dovedește. Cheile publice sunt publicate la
https://api.tracio.ai/.well-known/webhook-keys, indexate după kid.
Livrările de test trimise din dashboard sunt semnate doar cu HMAC — cheia privată de
platformă se află pe nodurile de livrare și, în mod deliberat, nu este disponibilă
dashboard-ului. Un verificator care impune strict Ed25519 trebuie să lase livrările
de test să treacă (ele poartă sufixul :test la eventId), altfel testele din
dashboard eșuează în timp ce producția este sănătoasă. Aceeași precauție se aplică și
verificărilor de format: o livrare de test poartă requestId de forma test_<hex> și
literalul test_visitor drept visitorId, așa că un handler care le validează față de
formele din producție va respinge o livrare altminteri bine formată.
După o rotație, ambele secrete rămân valabile 24 de ore, iar antetul poartă ambele semnături, astfel încât vă puteți actualiza configurația fără să pierdeți livrări. Acțiunea Revoke now scurtează fereastra. Actualizați secretul de partea dumneavoastră în 24 de ore: odată ce fereastra se închide, secretul vechi nu se mai potrivește, iar dacă endpoint-ul dumneavoastră răspunde la o semnătură invalidă cu 4xx, cinci astfel de răspunsuri la rând dezactivează webhook-ul.
| Antet | Descriere |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — două v1= într-o fereastră de rotație |
X-Tracio-Signature-Ed25519 | Semnătură de platformă, t=<unix>,kid=<id>,v1=<base64> (doar v2) |
X-Tracio-Event-Id | Identificatorul livrării — cheia de idempotență |
X-Tracio-Request-Id | Identificatorul vizitei (v2, doar evenimente de vizită) |
X-Tracio-Event-Type | Tipul evenimentului (doar v2) |
X-Tracio-Delivery-Attempt | Numărul încercării, începând de la 1 (doar v2) |
X-Tracio-Payload-Version | 2 (doar v2) |
X-Tracio-Webhook-Id | Identificatorul webhook-ului care a produs această livrare |
Livrările pot fi reîncercate, iar o reîncercare poartă același
X-Tracio-Event-Id. Deduplicați după el:
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")})Rețineți că eventId este unic per eveniment, nu per webhook: dacă mai multe
webhook-uri din workspace sunt abonate la același eveniment, fiecare primește o livrare
cu același identificator. El este construit ca <requestId>:<phase>, motiv pentru care
cele trei faze ale unei vizite se deduplică independent, în loc să se contopească
într-una singură.
Răspundeți cu 2xx — este singurul semn că o livrare a fost acceptată.
| Răspuns | Ce se întâmplă |
|---|---|
2xx | Livrare finalizată |
429 Too Many Requests | Nu se contorizează ca eșec și nu consumă o încercare; un Retry-After mai lung este respectat |
408, 425, 5xx, conexiune întreruptă | Se reîncearcă, cu o pauză tot mai mare |
410 Gone | Endpoint-ul este tratat ca eliminat — webhook-ul este dezactivat imediat |
Alte 4xx | Se reîncearcă, dar cinci la rând dezactivează webhook-ul — 400/401/404 nu se rezolvă prin reîncercare |
Programul reîncercărilor: 5 s → 30 s → 2 min → 10 min → 30 min → 2 h → 6 h (8 încercări). Primele reîncercări încap într-un minut, așa că o repornire scurtă a serviciului dumneavoastră nu vă costă o notificare. Fiecare pauză este randomizată între jumătate și valoarea întreagă, ca reîncercările să nu pornească toate deodată după o cădere.
Dezactivarea automată necesită atât un prag (20 de eșecuri consecutive sau 5 erori de configurare), cât și cel puțin 15 minute consecutive de eșecuri — o repornire scurtă nu poate ucide integrarea, chiar dacă multe livrări s-au adunat în coadă. Un interval mai mare de 15 minute reia numărătoarea. Dashboard-ul arată motivul, cu codul de răspuns și textul erorii, plus un buton Re-enable care resetează contoarele.
| Plan | Webhook-uri per workspace |
|---|---|
| Free | Indisponibil |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoint-urile trebuie să fie https, cu IP public — adresele private și de loopback
sunt respinse, inclusiv la o redirecționare — și să nu depășească două redirecționări
în adâncime.
Se urmează doar redirecționările 307 și 308. 301, 302 și 303 instruiesc
clientul să treacă la GET și să renunțe la corp, așa că o livrare nu le urmează, iar
încercarea se contorizează drept eșuată. Dacă load balancer-ul dumneavoastră
normalizează URL-ul (adăugând www sau o bară finală), îndreptați webhook-ul direct
către URL-ul final.
Webhook-urile se gestionează în dashboard. Dashboard-ul acționează un API de
administrare cu domeniu la nivel de workspace, servit pe host-ul aplicației (de exemplu
https://app.tracio.ai/api/v1), iar endpoint-urile de mai jos sunt exact cele pe care
le apelează. Fiecare endpoint de webhook se află sub /workspaces/{wsId}.
Aceasta nu este o suprafață server-la-server. API-ul de administrare acceptă doar JWT-ul sesiunii dumneavoastră de dashboard, verificat față de rolul dumneavoastră în workspace (RBAC); o cheie secretă
tracio_sk_…este respinsă aici. Întrucât acea sesiune trăiește în browser și expiră odată cu el, tratați apelurile de mai jos ca pe o descriere a ceea ce face dashboard-ul, nu ca pe o integrare de automatizat. Pentru acces programatic din propriul backend, folosiți Data API, care este doar pentru citire.
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": [] }'Secretul de semnare este generat de TRACIO și returnat o singură dată, la creare
(și la rotație), sub signingSecret. Păstrați-l în siguranță — este cheia cu care
verificați semnăturile.
{ "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" }}La citirile ulterioare signingSecret este mascat (null) — este dezvăluit doar de
creare și de rotația secretului.
| Metodă | Cale | Descriere |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Listarea webhook-urilor |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Actualizarea url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Ștergerea unui webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Trimiterea unei livrări de test semnate |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotirea secretului de semnare |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Listarea încercărilor recente de livrare |
Returnați un 2xx cât mai repede posibil și procesați payload-ul asincron, pentru a
evita timeout-urile:
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) }}Folosiți acțiunea Test de pe un webhook (sau POST .../webhooks/{webhookId}/test)
pentru a trimite către endpoint-ul dumneavoastră un payload de exemplu semnat și a
confirma că este accesibil și că verifică semnăturile corect.
Pentru dezvoltare locală, expuneți-vă serverul printr-un tunel precum ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint