ה-webhooks מוסרים אירועי זיהוי לשרת שלכם בזמן אמת. בכל פעם שמבקר מזוהה, TRACIO שולחת
בקשת POST ב-HTTP לכתובת ה-webhook שהגדרתם. גוף הבקשה הוא מטען (payload) האירוע.
הם גם הערוץ היחיד שמוסר הכרעות מאוחרות — אלה שבהן התנהגות המבקר הוכיחה שמדובר באוטומציה אחרי שהעמוד כבר נטען.
הגדירו אותם בלוח הבקרה תחת Settings → Webhooks. ה-webhooks דורשים תוכנית Pro ומעלה.
| אירוע | מתי | תוכנית |
|---|---|---|
identification | בכל ביקור — השלבים primary, late ו-correction | הכול |
account_takeover | ההתנהגות תחת חשבון כבר אינה תואמת לפרופיל בעליו | Business+ |
attack_detected | זינוק בכמות הבוטים באתר שלכם | Business+ |
reputation_changed | המוניטין של האדם שמאחורי מכשיר השתנה | Business+ |
שמות האירועים משתמשים בקו תחתון, לעולם לא בנקודות — אין visitor.created ואין
session.created. האירוע reputation_changed דורש את שכבת האדם, ולכן הוא נורה רק
עבור workspaces שבהם הופעל זיהוי זהות חוצה-מכשירים.
Webhook נרשם לסוגים מסוימים; הערך הנפרד * פירושו „כל סוג, כולל אלה שיתווספו בהמשך“.
סוג לא מוכר נדחה עם 400 בעת יצירת מנוי או עריכתו, כך שטעות הקלדה לא תשאיר אתכם עם
webhook שמעולם לא נורה בשקט.
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 | Webhooks שנוצרו לפני שגרסה 2 קיימת | נשארת עבורם ברירת המחדל |
2 | Webhooks חדשים | המתג בכרטיס ה-webhook בלוח הבקרה |
סכמה v1 מוקפאת — אף שדה בה אינו משתנה, ולכן אינטגרציות קיימות ממשיכות לעבוד ללא עריכות. כל מה שחדש חי ב-v2, וזו הסכמה שה-webhooks החדשים פולטים.
{ "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 | גרסת סכמת המטען (2) |
event | string | סוג האירוע |
eventId | string | מזהה המסירה — מפתח ה-idempotency |
requestId | string | מזהה הביקור (UUID), משותף לכל שלבי הביקור |
phase | string | primary, late, correction; account_takeover נושא beacon |
visitorId | string | מזהה מבקר יציב |
linkedId | string | מזהה מקושר שסופק על ידי הלקוח |
tag | string | תגית מותאמת שסופקה על ידי הלקוח |
timestamp | string | זמן האירוע (RFC 3339) |
url | string | כתובת העמוד שבו נלכד האירוע |
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 הוא המחמיר ביותר מבין כל ארבעת התרחישים.
| המלצה | תשלום | הרשמה | התחברות | שותפים (affiliate) |
|---|---|---|---|---|
allow | בצעו אותו | צרו אותו | אפשרו כניסה | זכו את ההמרה |
challenge | 3-D Secure / אישור | קפצ׳ה, אישור באימייל או בטלפון | 2FA מוגברת, אימות מחדש | סמנו כמפוקפקת עד שתתגלה פעילות |
review | בצעו, אך העבירו לתור לבדיקה | צרו עם הגבלות | אפשרו כניסה והפעילו התראה | עצרו את התשלום עד לבדיקה |
deny | אל תבצעו את העסקה | סרבו ליצור את החשבון | אל תאפשרו כניסה | אל תזכו את ההמרה |
version היא גרסת מערך הכללים — היא עולה ככל שהלוגיקה משתפרת. ה-guidance הוא מצטבר:
תרחישים חדשים מגיעים כמפתחות חדשים בלי לשבור את החוזה. השלב המאוחר מנצח, למעט
המלצה חלקית: מסירה שחושבה על מערך קלטים לא שלם מסומנת "partial": true, והמלצה
חלקית אינה דורסת המלצה מלאה שהתקבלה קודם לכן עבור אותו requestId. במסירה רגילה
השדה partial נעדר לחלוטין.
הספים המדויקים אינם מתועדים במכוון. המלצה שניתן להנדס לאחור לכדי ציון חדלה מלהיות הגנה.
account_takeoverBusiness ו-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 הוא חותמת הזמן של יוניקס (בשניות) שבה הבקשה נחתמה.v1 הוא HMAC-SHA256 של "<t>.<rawRequestBody>" בקידוד הקסדצימלי, עם מפתח שהוא
הסוד של ה-webhook שלכם.חותמת הזמן היא חלק מהתוכן הנחתם, וזה מה שמעניק הגנה מפני 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> ואת המילולי test_visitor בתור visitorId, כך שמטפל
שמאמת אותם מול הצורות של הייצור ידחה מסירה תקינה לחלוטין מכל בחינה אחרת.
לאחר החלפה שני הסודות נשארים תקפים 24 שעות והכותרת נושאת את שתי החתימות, כך שתוכלו לעדכן את התצורה שלכם בלי לאבד מסירות. הפעולה Revoke now מקצרת את החלון. עדכנו את הסוד אצלכם בתוך 24 שעות: ברגע שהחלון נסגר הסוד הישן מפסיק להתאים, ואם נקודת הקצה שלכם משיבה על חתימה לא תקפה בקוד 4xx, חמש תשובות כאלה ברצף משביתות את ה-webhook.
| כותרת | תיאור |
|---|---|
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 | מזהה המסירה — מפתח ה-idempotency |
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 | מזהה ה-webhook שהפיק את המסירה הזו |
מסירות עשויות להישלח שוב, וניסיון חוזר נושא את אותו 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 ייחודי לכל אירוע, לא לכל webhook: אם כמה webhooks ב-workspace
רשומים לאותו אירוע, כל אחד מהם יקבל מסירה עם אותו מזהה. הוא נבנה כ-<requestId>:<phase>,
ולכן שלושת שלבי הביקור עוברים דה-דופליקציה בנפרד זה מזה במקום להתמזג לאחד.
השיבו בקוד 2xx — זה הסימן היחיד לכך שמסירה התקבלה.
| תגובה | מה קורה |
|---|---|
2xx | המסירה הושלמה |
429 Too Many Requests | לא נספרת ככישלון ואינה מבזבזת ניסיון; Retry-After ארוך יותר יכובד |
408, 425, 5xx, ניתוק חיבור | נשלחת שוב עם השהיה גדלה |
410 Gone | נקודת הקצה נחשבת כמוסרת — ה-webhook מושבת מיידית |
4xx אחרים | נשלחת שוב, אך חמש ברצף משביתות את ה-webhook — 400/401/404 לא נרפאים בניסיון חוזר |
לוח הניסיונות החוזרים: 5 שנ׳ ← 30 שנ׳ ← 2 דק׳ ← 10 דק׳ ← 30 דק׳ ← 2 שע׳ ← 6 שע׳ (8 ניסיונות). הניסיונות הראשונים נכנסים בתוך דקה, כך שהפעלה מחדש קצרה של השירות שלכם לא עולה לכם בהתראה. כל השהיה מוגרלת בין מחצית הערך לערך המלא, כדי שניסיונות חוזרים לא יירו במטח אחד אחרי תקלה.
השבתה אוטומטית מחייבת גם סף (20 כשלים רצופים, או 5 שגיאות תצורה) וגם לפחות 15 דקות רצופות של כשלים — הפעלה מחדש קצרה אינה יכולה להרוג את האינטגרציה גם אם מסירות רבות המתינו בתור. פער ארוך מ-15 דקות מאפס את הספירה. לוח הבקרה מציג את הסיבה, לצד קוד התגובה וטקסט השגיאה, וכפתור Re-enable שמאפס את המונים.
| תוכנית | Webhooks לכל workspace |
|---|---|
| Free | לא זמין |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
נקודות הקצה חייבות להיות https עם כתובת IP ציבורית — כתובות פרטיות ו-loopback
נדחות, כולל בהפניה — ולא יותר משתי הפניות לעומק.
נעקבות רק הפניות 307 ו-308. 301, 302 ו-303 מורות ללקוח לעבור ל-GET
ולזרוק את הגוף, ולכן מסירה אינה עוקבת אחריהן והניסיון נספר ככישלון. אם מאזן העומסים
שלכם מנרמל את הכתובת (מוסיף www או לוכסן בסוף), הפנו את ה-webhook ישירות לכתובת
הסופית.
ה-webhooks מנוהלים בלוח הבקרה. לוח הבקרה מפעיל management API בהיקף workspace,
המוגש על מארח האפליקציה (למשל https://app.tracio.ai/api/v1), ונקודות הקצה שלהלן הן
בדיוק אלה שהוא קורא להן. כל נקודות הקצה של ה-webhooks נמצאות תחת /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 | רשימת webhooks |
PATCH | /workspaces/{wsId}/webhooks/{webhookId} | עדכון url / events / status |
DELETE | /workspaces/{wsId}/webhooks/{webhookId} | מחיקת webhook |
POST | /workspaces/{wsId}/webhooks/{webhookId}/test | שליחת מסירת בדיקה חתומה |
POST | /workspaces/{wsId}/webhooks/{webhookId}/secret/rotate | החלפת סוד החתימה |
GET | /workspaces/{wsId}/webhooks/{webhookId}/deliveries | רשימת ניסיונות מסירה אחרונים |
החזירו 2xx מהר ככל האפשר ועבדו את המטען באופן אסינכרוני כדי להימנע מ-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) }}השתמשו בפעולה Test על webhook (או ב-POST .../webhooks/{webhookId}/test) כדי
לשלוח מטען לדוגמה חתום לנקודת הקצה שלכם ולוודא שהיא נגישה ומאמתת חתימות כראוי.
לפיתוח מקומי, חשפו את השרת שלכם באמצעות מנהרה כמו ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint