Webhooks levererar identifieringshändelser till din server i realtid. Varje gång en
besökare identifieras skickar TRACIO en HTTP-POST-begäran till den webhook-URL du har
konfigurerat. Begärans body är händelsens payload.
De är dessutom den enda kanal som levererar sena verdikt — de där en besökares beteende visade att den var automatiserad först efter att sidan redan hade laddats.
Du ställer in dem i dashboarden under Settings → Webhooks. Webhooks kräver prisplanen Pro eller högre.
| Händelse | När | Prisplan |
|---|---|---|
identification | Vid varje besök — faserna primary, late och correction | Alla |
account_takeover | Beteendet under ett konto matchar inte längre innehavarens profil | Business+ |
attack_detected | En topp av bottar på din webbplats | Business+ |
reputation_changed | Ryktet för personen bakom en enhet har ändrats | Business+ |
Händelsenamn använder understreck, aldrig punkter — det finns inget visitor.created
eller session.created. reputation_changed kräver personlagret och utlöses därför
bara för workspaces där identitetsupplösning över flera enheter är aktiverad.
En webhook prenumererar på specifika typer; det separata värdet * betyder ”varje
typ, även de som läggs till senare”. En okänd typ avvisas med 400 när en
prenumeration skapas eller redigeras, så ett skrivfel kan inte lämna dig med en webhook
som tyst aldrig utlöser.
identificationEtt enda besök ger upp till tre leveranser som delar samma requestId:
primary — det första verdiktet, vid sidladdning.late — berikning ungefär nio sekunder senare, när de långsamma kontrollerna har landat.correction — en korrigering baserad på beteende (pekare, tangentbord, scrollning).Korrelera dem via requestId och skilj dem åt via phase. Den senare fasen har
företräde: om primary sa human och correction säger bot är den andra rätt
svar.
Förlita dig inte på ankomstordningen. Varje fas levereras oberoende och med eget
retry-schema — om primary gick in i retry medan late lyckades på första försöket
får du dem i omvänd ordning. Avgör företrädet utifrån fältet phase, inte utifrån
mottagningstidpunkten.
De tre är de enda faserna i en identification-händelse. Ett annat värde når dig
ändå: account_takeover bär phase: "beacon", eftersom ett larm om kontokapning
bara någonsin utlöses från en beteendebaserad beacon.
Lägg märke till diskrepansen det skapar, eftersom den påverkar idempotensen. En
identification-leverans i produktion har ett eventId som är exakt
<requestId>:<phase>, men två leveranser bryter mot den formeln. En
account_takeover är <requestId>:ato — suffixet är den bokstavliga strängen ato,
inte värdet i fältet phase. En testleverans som skickas från dashboarden är
<requestId>:test, medan phase i dess body enligt schema 2 fortfarande står som
primary — och en body enligt schema 1 har inget phase-fält alls, så headern är det
enda stället där suffixet syns. Använd eventId direkt som idempotensnyckel och sätt
aldrig ihop det på nytt av requestId och phase. Matcha på de värden du hanterar och
ignorera allt annat i stället för att avvisa leveransen.
attack_detected är en händelse på workspace-nivå: den har varken requestId eller
visitorId, och inget av blocken browser, geo, bot eller decision — de
nycklarna saknas helt enkelt. account_takeover produceras av ett specifikt besök och
bär hela identifieringskroppen för din prisplan plus ett accountAlert-block. Om du
parsar alla händelser i en och samma handler, kontrollera event innan du rör
besöksfält.
| Version | För vem | Hur du byter |
|---|---|---|
1 | Webhooks som skapades innan v2 fanns | Förblir standard för dem |
2 | Nya webhooks | Reglaget på webhook-kortet i dashboarden |
Schema v1 är fryst — inget av dess fält ändras, så befintliga integrationer fortsätter att fungera utan justeringar. Allt nytt lever i v2, som är det nya webhooks sänder.
{ "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", // grafikkortets modell, normaliserad; saknas när den är okänd "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-trafiken och den råa nätverksvägen går ut genom olika nätverk }, "bot": { "result": "human", "score": 3.2 }, // human | bot | uncertain "identification": { "confidence": 0.97, "incognito": false }, "decision": { "action": "real", "riskScore": 12.2 } // real | fake | suspicious}Noll- och tomvärden utelämnas. Sträng- och numeriska fält med värdet noll
(bot.type för en människa, till exempel) saknas i JSON:en — gör dem inte obligatoriska
i dina scheman och läs nästlade block defensivt.
bot.score och decision.riskScore är decimaltal på skalan 0..100 med en
decimal — exakt de tal som dashboarden rapporterar för samma besök. (I det frysta
v1-schemat använder de andra enheter: en andel 0..1 respektive 0..255.)
bot.type är antingen namnet på en känd bot eller en familjeetikett. Se
Bottyper för vokabuläret — interna kontrollnamn
exponeras aldrig, i ingen prisplan.
| Fält | Typ | Beskrivning |
|---|---|---|
version | number | Version av payload-schemat (2) |
event | string | Händelsetyp |
eventId | string | Identifierare för leveransen — idempotensnyckeln |
requestId | string | Identifierare för besöket (UUID), delad av alla dess faser |
phase | string | primary, late, correction; account_takeover bär beacon |
visitorId | string | Stabil besökaridentifierare |
linkedId | string | Länkad identifierare som klienten skickat med |
tag | string | Egen tagg som klienten skickat med |
timestamp | string | Händelsetid (RFC 3339) |
url | string | URL till sidan där händelsen fångades |
ip | string | Klientens IP-adress |
userAgent | string | Klientens råa user-agent-sträng |
browser.name / .version | string | Identifierad webbläsare |
os.name / .version | string | Identifierat operativsystem |
device | string | Enhetsklass (t.ex. desktop, mobile) |
gpu | string | Grafikkortets modell så som webbläsaren rapporterar den (WebGL), normaliserad till ett läsbart namn (Intel Iris Xe Graphics, Apple M1 Pro); Software renderer för programvarurasterare; saknas när den är okänd |
geo | object | IP-geolokalisering: country, city, lat, lon, timezone |
network | object | vpn, proxy, tor, datacenter (booleaner) och connectionType |
network.proxyDetected | boolean | Besökets HTTP-trafik och dess råa nätverksvägar går ut genom olika nätverk — en proxy eller ett VPN framför webbläsaren. Två adresser hos samma leverantör (carrier-NAT, en andra utgång i samma VPN) räknas inte |
bot.result | string | human, bot eller uncertain |
bot.type | string | Botnamn eller familjeetikett när en bot upptäcks |
bot.score | number | Botpoäng (0–100) |
identification.confidence | number | Identifieringens konfidens (0.0–1.0) |
identification.incognito | boolean | Privat/inkognito surfkontext |
decision.action | string | real, fake eller suspicious |
decision.riskScore | number | Aggregerad riskpoäng (0–100) |
Från Pro och uppåt — hur besökaren beter sig över tid:
{ "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}Från Business och uppåt — varför verdiktet blev som det blev:
{ "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, // den publika adress som observerats på den råa nätverksvägen, det vill // säga adressen bakom proxyn eller VPN:et; saknas när ingen sådan adress // observerats "realIp": { "address": "203.0.113.7", "country": "NL", "isp": "KPN" } }, // Skrivbordsmiljön som MÄTTS på en Linux-maskin ("Mint 22+", "Ubuntu", // "GNOME", "KDE"); en User-Agent kan inte uttrycka en distribution. Saknas // när den inte har fastställts — vid de flesta Linux-besök och vid varje // besök som inte är Linux. "osEnvironment": "Mint 22+", // Vad besöket påstod om sig självt jämfört med vad oberoende kontroller // mätte. Finns endast när en förfalskning faktiskt upptäckts; ett tomt // fält `real` betyder "kontrollen förblev tyst", aldrig "bekräftat". På // axeln `gpu` bär `claimed.gpu` det påstådda grafikkortet med samma // läsbara modellnamn som fältet `gpu` på översta nivån. "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"] } }, // Enhetsfakta — vad besökarens webbläsare rapporterar om maskinen, sanerat // på vår sida. screen: upplösning, färgdjup och device pixel ratio. // locale: webbläsarens egna föredragna språk och tidszon — till skillnad // från geo.timezone, som härleds från IP-adressen; en avvikelse mellan de // två är ett vanligt tecken på en förfalskad plats. clientHints: // User-Agent Client Hints — CPU-arkitektur och bitness, enhetsmodell // (Android: modellkoden i `model`, t.ex. "SM-A556B", och dess // marknadsnamn från Google Plays enhetslista i `deviceName`, t.ex. // "Samsung Galaxy A55 5G") och den exakta plattformsversionen; endast // Chromium-baserade webbläsare rapporterar dem. Varje block saknas när // besöket inte bar med sig sådana data, så behandla vart och ett som // valfritt. "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" }, // Finns endast när grafikkortet angav sig självt som virtuellt; hypervisor // är en sluten ordlista (vmware, virtualbox, parallels, qemu, hyperv, // bochs, intel-gvt, vgpu). Ett block som saknas betyder att inget sådant // belägg finns. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}Se Bot-detektering för vokabuläret av
orsakskoder och vad severity betyder.
guidance bär färdiga ”vad du bör göra”-rekommendationer per integrationspunkt, så att
du slipper härleda en policy ur råa poäng:
{ "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 }}Varje scenario startar på allow och kan bara röra sig uppåt på stegen:
allow → challenge → review → deny. Inom ett scenario vinner den strängaste axel
som utlöser, och overall är den strängaste av alla fyra scenarierna.
| Råd | Betalning | Registrering | Inloggning | Affiliate |
|---|---|---|---|---|
allow | Genomför den | Skapa det | Släpp in | Kreditera konverteringen |
challenge | 3-D Secure / bekräftelse | Captcha, bekräftelse via e-post eller telefon | Step-up-2FA, autentisera på nytt | Markera som tveksam tills aktivitet syns |
review | Genomför, men lägg i kö för granskning | Skapa med begränsningar | Släpp in och larma | Håll inne utbetalningen tills den granskats |
deny | Genomför inte transaktionen | Neka att skapa kontot | Släpp inte in | Kreditera inte konverteringen |
version är versionen av regeluppsättningen — den räknas upp allteftersom logiken
förbättras. Guidance är additivt: nya scenarier tillkommer som nya nycklar utan att
bryta kontraktet. Den senare fasen vinner, utom för partiella råd: en leverans som
beräknats på en ofullständig uppsättning indata är märkt med "partial": true, och ett
partiellt råd åsidosätter inte ett fullständigt råd som tagits emot tidigare för
samma requestId. I en vanlig leverans saknas fältet partial helt.
Exakta trösklar dokumenteras avsiktligt inte. Ett råd som kan bakåtkompileras till en poäng upphör att vara ett skydd.
account_takeoverEndast Business och Enterprise. Kroppen är hela identifieringskuvertet för din prisplan
plus ett accountAlert-block, levererat högst en gång per besök:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // larmet utlöses från en beacon; bara eventId säger "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}I v1 bär det här blocket type, linkedId och drift; i v2 byter två fält namn —
linkedId → accountId och drift → driftScore. Uppdatera din handler när du byter
payloadVersion, annars slutar din logik för kontokapning tyst att se datan.
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 }}Varje leverans innehåller en X-Tracio-Signature-header:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed…t är Unix-tidsstämpeln (i sekunder) då begäran signerades.v1 är den hex-kodade HMAC-SHA256:n av "<t>.<rawRequestBody>", nycklad med din
webhook-hemlighet.Tidsstämpeln ingår i det signerade innehållet, vilket ger skydd mot replay.
Två saker måste bli rätt, annars misslyckas verifieringen i produktion:
v1=-värde som helst. Under en hemlighetsrotation bär
headern två signaturer, och en parser som bara behåller en av dem avvisar giltiga
leveranser under hela rotationsfönstret.// 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")})Leveranser enligt schema 2 bär dessutom X-Tracio-Signature-Ed25519
(t=<unix>,kid=<id>,v1=<base64>). Båda sidor känner till HMAC-hemligheten, så HMAC:en
bevisar att avsändaren känner till hemligheten, men inte att begäran kommer från
TRACIO; det gör den asymmetriska signaturen. De publika nycklarna publiceras på
https://api.tracio.ai/.well-known/webhook-keys, indexerade på kid.
Testleveranser som skickas från dashboarden signeras enbart med HMAC — plattformens
privata nyckel finns på leveransnoderna och är avsiktligt inte tillgänglig för
dashboarden. En verifierare som hårt kräver Ed25519 måste släppa igenom
testleveranser (de bär suffixet :test på eventId), annars misslyckas testerna
från dashboarden medan produktionen mår bra. Samma försiktighet gäller
formatkontroller: en testleverans bär requestId på formen test_<hex> och den
bokstavliga strängen test_visitor som visitorId, så en handler som validerar dem
mot produktionsformerna avvisar en leverans som i övrigt är helt korrekt.
Efter en rotation förblir båda hemligheterna giltiga i 24 timmar och headern bär båda signaturerna, så du kan uppdatera din konfiguration utan att förlora leveranser. Åtgärden Revoke now kortar fönstret. Uppdatera hemligheten på din sida inom 24 timmar: när fönstret stängs slutar den gamla hemligheten att matcha, och om din endpoint svarar på en ogiltig signatur med 4xx stänger fem sådana svar i rad av webhooken.
| Header | Beskrivning |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> — två v1= under ett rotationsfönster |
X-Tracio-Signature-Ed25519 | Plattformssignatur, t=<unix>,kid=<id>,v1=<base64> (endast v2) |
X-Tracio-Event-Id | Identifierare för leveransen — idempotensnyckeln |
X-Tracio-Request-Id | Identifierare för besöket (v2, endast besökshändelser) |
X-Tracio-Event-Type | Händelsetypen (endast v2) |
X-Tracio-Delivery-Attempt | Försöksnummer, med start på 1 (endast v2) |
X-Tracio-Payload-Version | 2 (endast v2) |
X-Tracio-Webhook-Id | Identifierare för webhooken som producerade denna leverans |
Leveranser kan göras om, och ett omförsök bär samma X-Tracio-Event-Id. Deduplicera på
den:
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")})Observera att eventId är unikt per händelse, inte per webhook: om flera webhooks
i workspacet prenumererar på samma händelse får var och en en leverans med samma
identifierare. Det byggs som <requestId>:<phase>, vilket är skälet till att ett
besöks tre faser dedupliceras oberoende av varandra i stället för att falla ihop till
en enda.
Svara med 2xx — det är det enda tecknet på att en leverans har tagits emot.
| Svar | Vad som händer |
|---|---|
2xx | Leveransen slutförd |
429 Too Many Requests | Räknas inte som misslyckande och förbrukar inget försök; ett längre Retry-After respekteras |
408, 425, 5xx, bruten anslutning | Görs om med växande paus |
410 Gone | Endpointen behandlas som borttagen — webhooken stängs av omedelbart |
Övriga 4xx | Görs om, men fem i rad stänger av webhooken — 400/401/404 botas inte av omförsök |
Omförsöksschema: 5 s → 30 s → 2 min → 10 min → 30 min → 2 h → 6 h (8 försök). De första omförsöken ryms inom en minut, så en kort omstart av din tjänst kostar dig ingen avisering. Varje paus randomiseras mellan halva och hela värdet så att omförsöken inte avfyras i en enda salva efter ett avbrott.
Automatisk avstängning kräver både ett tröskelvärde (20 misslyckanden i följd, eller 5 konfigurationsfel) och minst 15 sammanhängande minuter av misslyckanden — en kort omstart kan inte döda integrationen ens om många leveranser låg i kö. Ett glapp längre än 15 minuter startar om räkningen. Dashboarden visar orsaken, med svarskod och feltext, samt en knapp Re-enable som nollställer räknarna.
| Prisplan | Webhooks per workspace |
|---|---|
| Free | Ej tillgängligt |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
Endpoints måste vara https med publik IP — privata adresser och loopback-adresser
avvisas, även vid en omdirigering — och inte djupare än två omdirigeringar.
Endast omdirigeringarna 307 och 308 följs. 301, 302 och 303 instruerar
klienten att byta till GET och kasta bort bodyn, så en leverans följer dem inte och
försöket räknas som misslyckat. Om din lastbalanserare normaliserar URL:en (lägger till
www eller ett avslutande snedstreck), peka webhooken direkt på den slutliga URL:en.
Webhooks hanteras i dashboarden. Dashboarden driver ett workspace-avgränsat
hanterings-API som tillhandahålls på applikationsvärden (till exempel
https://app.tracio.ai/api/v1), och endpointerna nedan är just de den anropar. Alla
webhook-endpoints ligger under /workspaces/{wsId}.
Detta är ingen server-till-server-yta. Hanterings-API:et accepterar bara din dashboard-session-JWT, kontrollerad mot din workspace-roll (RBAC); en hemlig nyckel av typen
tracio_sk_…avvisas här. Eftersom den sessionen lever i webbläsaren och upphör med den bör du se anropen nedan som en beskrivning av vad dashboarden gör, snarare än som en integration att automatisera. För programmatisk åtkomst från din egen backend, använd det skrivskyddade Data-API:et.
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": [] }'Signeringshemligheten genereras av TRACIO och returneras en gång vid skapandet (och
vid rotation) under signingSecret. Förvara den säkert — det är nyckeln du använder för
att verifiera signaturer.
{ "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" }}Vid senare läsningar är signingSecret maskerad (null) — den avslöjas endast av
skapande och rotation av hemligheten.
| Metod | Sökväg | Beskrivning |
|---|---|---|
GET | /workspaces/{wsId}/webhooks | Lista webhooks |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | Uppdatera url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | Ta bort en webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | Skicka en signerad testleverans |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | Rotera signeringshemligheten |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | Lista senaste leveransförsök |
Returnera ett 2xx så snabbt som möjligt och bearbeta payloaden asynkront för att
undvika timeouts:
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) }}Använd åtgärden Test på en webhook (eller POST .../webhooks/{webhookId}/test) för
att skicka en signerad exempel-payload till din endpoint och bekräfta att den är
nåbar och verifierar signaturer korrekt.
För lokal utveckling exponerar du din server med en tunnel som ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint