توصّل الـ webhooks أحداث التعرّف إلى خادمك في الوقت الفعلي. في كل مرة يتم فيها
التعرّف على زائر، يرسل TRACIO طلب HTTP من نوع POST إلى عنوان الـ webhook الذي
ضبطته. جسم الطلب هو حمولة الحدث نفسها — لا يوجد أي غلاف يلفّها.
الجسم عبارة عن مستند JSON مسطّح بصيغة camelCase. وهو مجموعة فرعية عامة منتقاة
من الحدث الداخلي — دون بصمات hash أو علامات كشف داخلية.
{ "requestId": "1710432000_abc123def", "phase": "primary", "visitorId": "X7fh2Hg9LkMn3pQr", "linkedId": "user_12345", "tag": "login", "timestamp": "2024-03-12T16:00:00Z", "url": "https://your-app.com/login", "ip": "94.142.239.124", "userAgent": "Mozilla/5.0 …", "browser": { "name": "Chrome", "version": "120.0" }, "os": { "name": "macOS", "version": "14.3" }, "device": "desktop", "geo": { "country": "CZ", "city": "Prague", "lat": 50.05, "lon": 14.4, "timezone": "Europe/Prague", "isp": "Example ISP" }, "network": { "vpn": false, "proxy": false, "tor": false, "datacenter": false, "connectionType": "residential" }, "bot": { "result": "human", "type": "", "score": 0.02 }, "identification": { "confidence": 0.95, "incognito": false, "visitType": "returning" }, "decision": { "action": "allow", "riskScore": 4, "suspectScore": 0.08 }}| الحقل | النوع | الوصف |
|---|---|---|
requestId | string | معرّف الحدث الفريد (وهو أيضاً مفتاح idempotency) |
phase | string | primary أو late — انظر أدناه |
visitorId | string | معرّف الزائر الثابت |
linkedId | string | معرّف مرتبط يوفّره العميل |
tag | string | وسم مخصّص يوفّره العميل |
timestamp | string | وقت الحدث (RFC 3339) |
url | string | عنوان URL للصفحة التي التُقط عندها الحدث |
ip | string | عنوان IP الخاص بالعميل |
userAgent | string | سلسلة user-agent الخام للعميل |
browser.name / .version | string | المتصفّح المكتشَف |
os.name / .version | string | نظام التشغيل المكتشَف |
device | string | فئة الجهاز (مثل desktop أو mobile) |
geo | object | تحديد الموقع عبر IP: country, city, lat, lon, timezone, isp |
network | object | vpn, proxy, tor, datacenter (قيم منطقية) وconnectionType |
bot.result | string | human أو bot أو uncertain |
bot.type | string | تسمية أتمتة حرّة النص عند اكتشاف روبوت |
bot.score | number | درجة احتمال كون الزائر روبوتاً |
identification.confidence | number | ثقة النموذج (0.0–1.0) |
identification.incognito | boolean | سياق تصفّح خاص/متخفٍّ |
identification.visitType | string | تصنيف الزيارة |
decision.action | string | الإجراء الموصى به |
decision.riskScore | number | درجة الخطر الإجمالية (0–100) |
decision.suspectScore | number | درجة اشتباه دقيقة |
يميّز الحقل phase بين عمليتَي تسليم تتشاركان نفس requestId:
primary — تُرسل فوراً عند التعرّف على الزائر.late — حدث إثراء لاحق (حكم مُنقَّح بشأن الروبوتات وإشارات سلوكية التُقطت
بعد ذلك بقليل).اربط بين الاثنين عبر requestId وميّز بينهما عبر phase.
تتضمّن كل عملية تسليم ترويسة X-Tracio-Signature:
X-Tracio-Signature: t=1710432000,v1=5257a869e7ecebed...t هو الطابع الزمني Unix (بالثواني) الذي جرى فيه توقيع الطلب.v1 هو HMAC-SHA256 مُرمَّز بالنظام الست عشري لـ "<t>.<rawRequestBody>"،
مُوقَّع بمفتاح هو webhook secret الخاص بك.الطابع الزمني جزء من المحتوى المُوقَّع، وهو ما يوفّر الحماية من إعادة التشغيل —
تحقّق مقابل جسم الطلب الخام بصيغة Buffer (لا تستخدم JSON المُحلَّل/المُعاد
تسلسله، وإلا فلن يتطابق التوقيع). كوّن المحتوى المُوقَّع كبايتات: البادئة "<t>."
موصولة ببافر الجسم الخام، ثم احسب HMAC عليه.
// Express.js exampleimport express from "express"import crypto from "crypto"
const app = express()
// Capture the raw body so we can verify the signature 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 { // Parse "t=...,v1=..." const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string])) const t = parts["t"] const v1 = parts["v1"] if (!t || !v1) return false
// Sign the raw bytes: "<t>." prefix + the raw request body buffer. const signed = Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]) const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex")
const a = Buffer.from(v1, "hex") const b = Buffer.from(expected, "hex") return a.length === b.length && crypto.timingSafeEqual(a, b)}
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")})قد ترغب أيضاً في رفض عمليات التسليم التي يكون فيها t بعيداً جداً عن الوقت
الحالي (على سبيل المثال، بفارق يزيد على خمس دقائق) كإجراء إضافي للحماية من إعادة
التشغيل.
| الترويسة | الوصف |
|---|---|
Content-Type | application/json |
X-Tracio-Signature | t=<unix>,v1=<hmac_sha256_hex> على "<t>.<rawBody>" |
X-Tracio-Event-Id | قيمة requestId للحدث — استخدمها كمفتاح idempotency |
X-Tracio-Webhook-Id | معرّف الـ webhook الذي أنتج عملية التسليم هذه |
أبسط طريقة لإدارة الـ webhooks هي بصرياً من لوحة التحكم. كما يمكن إدارتها
برمجياً عبر واجهة الإدارة المحصورة بنطاق مساحة العمل (workspace-scoped) — وهي نفس
الواجهة التي تستخدمها لوحة التحكم. تُقدَّم على مضيف التطبيق (على سبيل المثال
https://app.tracio.ai/api/v1) وتُصادَق باستخدام رمز JWT الخاص بجلسة لوحة
التحكم (Clerk)؛ كما يُتحقَّق من كل طلب إضافياً مقابل دورك في مساحة العمل (RBAC).
لا يوجد سرّ API مستقل. تقع جميع نقاط نهاية الـ webhook تحت
/workspaces/{workspaceId}.
curl -X POST https://app.tracio.ai/api/v1/workspaces/{workspaceId}/webhooks \ -H "Authorization: Bearer <clerk-session-jwt>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhook/tracio", "events": [] }'يُولِّد TRACIO سرّ التوقيع (signing secret) ويُعاد مرة واحدة عند الإنشاء (وعند
التدوير) تحت signingSecret. خزّنه بأمان — فهو المفتاح الذي تستخدمه للتحقّق من
التواقيع.
{ "ok": true, "data": { "id": "wh_abc123", "workspaceEnvironmentId": "b201f2ba-…", "url": "https://your-server.com/webhook/tracio", "events": [], "signingSecret": "f3a9…<hex>", "status": "active", "successRate": 100, "createdAt": "2024-03-12T16:00:00Z" }}في عمليات القراءة اللاحقة يكون signingSecret محجوباً (null) — ولا يُكشف إلا
عند الإنشاء وعند تدوير السرّ.
| الطريقة | المسار | الوصف |
|---|---|---|
GET | /workspaces/{workspaceId}/webhooks | سرد الـ webhooks |
PATCH | /workspaces/{workspaceId}/webhooks/{webhookId} | تحديث url / events / status |
DELETE | /workspaces/{workspaceId}/webhooks/{webhookId} | حذف webhook |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/test | إرسال عملية تسليم اختبارية مُوقَّعة |
POST | /workspaces/{workspaceId}/webhooks/{webhookId}/secret/rotate | تدوير سرّ التوقيع |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | سرد آخر محاولات التسليم |
GET | /workspaces/{workspaceId}/webhooks/{webhookId}/deliveries | سرد آخر محاولات التسليم |
قد يُعاد محاولة عمليات التسليم، لذا ينبغي أن يكون معالِجك idempotent. أزِل
التكرار بالاعتماد على ترويسة X-Tracio-Event-Id (أي requestId):
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")})أعِد استجابة 2xx بأسرع ما يمكن وعالِج الحمولة بشكل غير متزامن لتجنّب انتهاء
المهلات. تُعاد محاولة الاستجابات غير 2xx (وأخطاء الاتصال)؛ ويمكن للإخفاقات
المتكرّرة أن تعطّل الـ webhook تلقائياً.
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