تسلّم الـ webhooks أحداث التعرّف إلى خادمك في الوقت الفعلي. في كل مرة يجري فيها
التعرّف على زائر، يرسل TRACIO طلب POST عبر HTTP إلى عنوان الـ webhook الذي ضبطته.
وجسم الطلب هو نفسه حمولة الحدث.
وهي أيضًا القناة الوحيدة التي تسلّم الأحكام المتأخرة: تلك التي أثبت فيها سلوك الزائر أنه آلي بعد أن كانت الصفحة قد حُمّلت بالفعل.
اضبطها من لوحة التحكم في Settings → Webhooks. تتطلب الـ webhooks خطة Pro أو أعلى.
| الحدث | متى | الخطة |
|---|---|---|
identification | في كل زيارة — مراحل primary وlate وcorrection | الكل |
account_takeover | لم يعد السلوك تحت الحساب مطابقًا لملف صاحبه | Business+ |
attack_detected | ارتفاع مفاجئ في عدد البوتات على موقعك | Business+ |
reputation_changed | تغيّرت سمعة الشخص الذي يقف خلف الجهاز | Business+ |
تستخدم أسماء الأحداث الشرطة السفلية لا النقاط أبدًا — فلا وجود لـ visitor.created ولا
لـ session.created. ويتطلب reputation_changed طبقة الشخص، ولذلك لا يُطلق إلا في
مساحات العمل التي جرى فيها تفعيل تحديد الهوية عبر الأجهزة.
يشترك الـ webhook في أنواع محددة؛ أما القيمة المنفصلة * فتعني «كل نوع، بما في ذلك
الأنواع التي تُضاف لاحقًا». ويُرفض أي نوع غير معروف بالرمز 400 عند إنشاء الاشتراك أو
تعديله، فلا يتركك خطأ مطبعي مع webhook لا يُطلق أبدًا في صمت.
identificationتنتج الزيارة الواحدة ما يصل إلى ثلاث عمليات تسليم تتشارك القيمة نفسها لـ requestId:
primary — الحكم الأولي، عند تحميل الصفحة.late — إثراء بعد نحو تسع ثوانٍ، متى وصلت الفحوصات البطيئة.correction — تصحيح مبني على السلوك (المؤشر، لوحة المفاتيح، التمرير).اربطها ببعضها عبر requestId، وميّز بينها عبر phase. المرحلة الأحدث هي المقدَّمة:
إذا قالت primary إنه human وقالت correction إنه bot، فالإجابة الصحيحة هي
الثانية.
لا تعتمد على ترتيب الوصول. فكل مرحلة تُسلَّم بشكل مستقل ووفق جدول إعادة محاولات
خاص بها: إذا دخلت primary في إعادة المحاولة بينما نجحت late من المحاولة الأولى،
فستصلك بترتيب معكوس. حدّد الأولوية من حقل phase، لا من وقت الاستلام.
هذه الثلاث هي المراحل الوحيدة لحدث identification. وتصلك قيمة أخرى واحدة: إذ يحمل
account_takeover القيمة phase: "beacon"، لأن التنبيه إلى الاستيلاء على الحساب لا
يُرفع إلا من beacon سلوكي.
انتبه إلى التعارض الذي ينشأ عن ذلك، لأنه يمسّ عدم التكرار (idempotency). فعملية تسليم
identification في الإنتاج يكون eventId فيها بالضبط <requestId>:<phase>، لكن
عمليتَي تسليم تكسران هذه الصيغة. فـ account_takeover يكون <requestId>:ato —
واللاحقة هنا هي الحرفية ato، لا قيمة حقل phase. وعملية التسليم التجريبية المرسلة من
لوحة التحكم تكون <requestId>:test، بينما يظل phase في جسمها بمخطط 2 يقول primary
— أما جسم مخطط 1 فلا يحتوي على حقل phase إطلاقًا، فلا تظهر تلك اللاحقة إلا في الترويسة.
استخدم eventId مفتاحًا لعدم التكرار مباشرةً، ولا تعِد تركيبه أبدًا من requestId
وphase. طابق على القيم التي تعالجها وتجاهل ما عداها بدل أن ترفض عملية التسليم.
attack_detected حدث على مستوى مساحة العمل: ليس له requestId ولا visitorId ولا أي
من كتل browser أو geo أو bot أو decision — هذه المفاتيح غائبة ببساطة. أما
account_takeover فينتج عن زيارة بعينها ويحمل جسم التعرّف الكامل الخاص بخطتك بالإضافة
إلى كتلة accountAlert. وإذا كنت تحلّل كل الأحداث في معالج واحد، فتحقّق من event قبل
أن تلمس حقول الزيارة.
| الإصدار | لمن | كيفية التبديل |
|---|---|---|
1 | الـ webhooks المنشأة قبل وجود الإصدار 2 | يبقى الافتراضي بالنسبة لها |
2 | الـ webhooks الجديدة | المفتاح على بطاقة الـ webhook في لوحة التحكم |
مخطط الإصدار 1 مجمَّد — لا يتغير أي من حقوله، لذا تظل عمليات التكامل القائمة تعمل دون تعديل. وكل ما هو جديد يعيش في الإصدار 2، وهو ما تُصدره الـ 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 بخانة واحدة بعد
الفاصلة العشرية — وهي بالضبط الأرقام التي تعرضها لوحة التحكم للزيارة نفسها. (وفي مخطط
الإصدار 1 المجمَّد تستخدم وحدات مختلفة: كسر على 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 — معمارية المعالج وعرضه بالبتّات، وطراز الجهاز (على // Android: رمز الطراز في `model`، مثل "SM-A556B"، واسمه التسويقي من قائمة // أجهزة Google Play في `deviceName`، مثل "Samsung Galaxy A55 5G")، وإصدار // المنصّة الدقيق؛ ولا تبلّغ عنها إلا المتصفحات المبنية على Chromium. // وكل كتلة تغيب عندما لا تحمل الزيارة بيانات كهذه، فتعامل مع كلٍّ منها على // أنها اختيارية. "screen": { "width": 2560, "height": 1600, "colorDepth": 30, "pixelRatio": 2 }, "locale": { "languages": ["en-US", "de"], "timezone": "Europe/Berlin" }, "clientHints": { "architecture": "arm", "bitness": "64", "platformVersion": "15.5.0" }, // موجود فقط عندما يعرّف مُهايئ الفيديو نفسه بأنه افتراضي؛ وhypervisor قاموس // مغلق (vmware، virtualbox، parallels، qemu، hyperv، bochs، intel-gvt، // vgpu). وغياب الكتلة يعني أنه لا دليل من هذا القبيل. "environment": { "virtualMachine": true, "hypervisor": "vmware" }, "decision": { "suspectScore": 55 }, "guidance": { "version": 1, "overall": "review" } // see below}راجع كشف البوتات للاطلاع على مفردات رموز
الأسباب وعلى معنى severity.
يحمل guidance توصيات جاهزة عن «ما العمل» لكل نقطة تكامل، حتى لا تضطر إلى اشتقاق
سياسة من الدرجات الخام:
{ "guidance": { "version": 1, "overall": "review", // the strictest advice across the scenarios "payment": "review", // whether to accept the payment "registration": "challenge", // whether to create the account "login": "challenge", // whether to let them into the account "affiliate": "review", // whether to credit the conversion to the partner "basis": ["risk", "network"] // the axes that determined the advice }}يبدأ كل سيناريو من allow ولا يتحرك على السلّم إلا صعودًا:
allow ← challenge ← review ← deny. وداخل السيناريو الواحد يفوز أشدّ محور جرى
تفعيله، بينما overall هو الأشدّ عبر السيناريوهات الأربعة كلها.
| التوصية | الدفع | التسجيل | تسجيل الدخول | التسويق بالعمولة |
|---|---|---|---|---|
allow | نفّذه | أنشئه | اسمح بالدخول | احتسب التحويل |
challenge | 3-D Secure / تأكيد | كابتشا أو تأكيد بالبريد أو الهاتف | 2FA إضافية، إعادة مصادقة | ضَعه في خانة المشكوك فيه حتى يظهر نشاط |
review | نفّذه، لكن أدرجه في قائمة المراجعة | أنشئه مع قيود | اسمح بالدخول، وأطلق تنبيهًا | احجز الدفعة حتى تتم المراجعة |
deny | لا تنفّذ المعاملة | ارفض إنشاء الحساب | لا تسمح بالدخول | لا تحتسب التحويل |
version هو إصدار مجموعة القواعد — ويُرفع كلما تحسّن المنطق. وguidance تراكمي:
تصل السيناريوهات الجديدة كمفاتيح جديدة دون كسر العقد. المرحلة الأحدث تفوز، باستثناء
التوصية الجزئية: فالتسليم المحسوب على مجموعة مدخلات ناقصة يُوسَم بـ "partial": true،
والتوصية الجزئية لا تُلغي توصية كاملة وردت قبلها لنفس requestId. وفي التسليم
العادي لا يوجد حقل partial إطلاقًا.
لا تُوثَّق العتبات الدقيقة عن قصد. فالتوصية التي يمكن ردّها بالهندسة العكسية إلى درجة تكفّ عن كونها دفاعًا.
account_takeoverلخطتَي Business وEnterprise فقط. الجسم هو مغلّف التعرّف الكامل الخاص بخطتك بالإضافة إلى
كتلة accountAlert، ويُسلَّم مرة واحدة على الأكثر لكل زيارة:
{ "version": 2, "event": "account_takeover", "eventId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31:ato", "requestId": "9c1f6a2e-3b7d-4c58-a1e2-6f0d8b4a7c31", "phase": "beacon", // التنبيه يُرفع من beacon؛ وحده eventId يقول "ato" "accountAlert": { "type": "behavior-drift", "accountId": "user-42", // your linkedId for the account "driftScore": 0.83 } // …the remaining identification fields}في الإصدار 1 تحمل هذه الكتلة type وlinkedId وdrift؛ وفي الإصدار 2 أُعيدت تسمية
حقلين — 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 الخاص بك.طابع الوقت جزء من المحتوى الموقَّع، وهذا ما يمنح الحماية من إعادة التشغيل.
أمران يجب ضبطهما، وإلا فشل التحقّق في بيئة الإنتاج:
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> (الإصدار 2 فقط) |
X-Tracio-Event-Id | معرّف التسليم — مفتاح idempotency |
X-Tracio-Request-Id | معرّف الزيارة (الإصدار 2، أحداث الزيارة فقط) |
X-Tracio-Event-Type | نوع الحدث (الإصدار 2 فقط) |
X-Tracio-Delivery-Attempt | رقم المحاولة، بدءًا من 1 (الإصدار 2 فقط) |
X-Tracio-Payload-Version | 2 (الإصدار 2 فقط) |
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 في مساحة
العمل بالحدث نفسه، تلقّى كل منها عملية تسليم بالمعرّف ذاته. وهو يُبنى على صورة
<requestId>:<phase>، ولهذا تُزال التكرارات لمراحل الزيارة الثلاث كلٌّ على حدة بدل أن
تنطوي في واحدة.
استجب بالرمز 2xx — فهو العلامة الوحيدة على قبول عملية التسليم.
| الاستجابة | ماذا يحدث |
|---|---|
2xx | اكتمل التسليم |
429 Too Many Requests | لا تُحتسب فشلًا ولا تستهلك محاولة؛ ويُحترم Retry-After الأطول |
408 و425 و5xx وانقطاع الاتصال | تُعاد المحاولة مع فترة انتظار متزايدة |
410 Gone | تُعامَل نقطة النهاية كأنها أُزيلت — ويُعطَّل الـ webhook فورًا |
بقية رموز 4xx | تُعاد المحاولة، لكن خمسًا متتالية تُعطّل الـ webhook — و400/401/404 لا تُعالَج بالإعادة |
جدول إعادة المحاولات: 5 ثوانٍ ← 30 ثانية ← دقيقتان ← 10 دقائق ← 30 دقيقة ← ساعتان ← 6 ساعات (8 محاولات). المحاولات الأولى تقع داخل دقيقة واحدة، لذا لا تكلّفك إعادة تشغيل قصيرة لخدمتك أي إشعار. وتُوزَّع كل فترة انتظار عشوائيًا بين نصف القيمة وقيمتها الكاملة حتى لا تنطلق المحاولات دفعة واحدة بعد انقطاع.
التعطيل التلقائي يتطلب في آنٍ واحد بلوغ عتبة (20 إخفاقًا متتاليًا، أو 5 أخطاء إعداد) و 15 دقيقة متواصلة على الأقل من الإخفاقات — فإعادة تشغيل قصيرة لا يمكن أن تقتل التكامل حتى لو تراكمت عمليات تسليم كثيرة في الطابور. وأي فجوة تتجاوز 15 دقيقة تعيد العدّ من الصفر. وتعرض لوحة التحكم السبب مع رمز الاستجابة ونص الخطأ، وزرّ Re-enable الذي يصفّر العدّادات.
| الخطة | عدد الـ webhooks لكل مساحة عمل |
|---|---|
| Free | غير متاح |
| Pro | 5 |
| Business | 20 |
| Enterprise | 100 |
يجب أن تكون نقاط النهاية على https وبعنوان IP عام — إذ تُرفض العناوين الخاصة وعناوين
الاسترجاع (loopback)، بما في ذلك عند إعادة التوجيه — وبعمق لا يتجاوز إعادتَي توجيه.
لا تُتَّبع سوى إعادتَي التوجيه 307 و308. أما 301 و302 و303 فتوجّه العميل
إلى التحول إلى GET وإسقاط الجسم، لذا لا تتبعها عملية التسليم وتُحتسب المحاولة فاشلة.
وإذا كان موازن الحمل لديك يطبّع العنوان (بإضافة www أو شرطة مائلة في النهاية)، فوجّه
الـ webhook مباشرة إلى العنوان النهائي.
تُدار الـ webhooks من لوحة التحكم. وتشغّل لوحة التحكم واجهة API للإدارة على نطاق
مساحة العمل، تُقدَّم على مضيف التطبيق (مثلًا https://app.tracio.ai/api/v1)، ونقاط
النهاية أدناه هي بالضبط ما تستدعيه. وتقع كل نقاط نهاية الـ webhooks تحت
/workspaces/{wsId}.
هذه ليست واجهة للتعامل بين الخوادم. فواجهة الإدارة لا تقبل سوى رمز JWT الخاص بـ جلسة لوحة التحكم، ويُفحَص مقابل دورك في مساحة العمل (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 بأسرع ما يمكن وعالِج الحمولة بشكل غير متزامن لتجنّب انتهاء المهلة:
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 على أحد الـ webhooks (أو
POST .../webhooks/{webhookId}/test) لإرسال حمولة نموذجية موقَّعة إلى نقطة النهاية
لديك والتأكد من أنها قابلة للوصول وتتحقق من التواقيع بشكل صحيح.
وللتطوير المحلي، أتِح خادمك عبر نفق مثل ngrok:
ngrok http 3000# Use the generated URL as your webhook endpoint