تدفع خطافات الويب كل تغيير في مساحة عملك إلى أنظمتك لحظة حدوثه: سجل أُنشئ في التطبيق، أو مرشح انتقل إلى مرحلة جديدة، أو موظف أُنهيت خدمته، أو صف كُتب في مهمة إدراج مجمّع. يصبح كل تغيير حدثًا، ويرسله جديرون طلب POST موقّعًا إلى كل نقطة استقبال مشتركة في نوعه.
تأتي الأحداث من سجل التدقيق، فهي تغطي التغييرات في التطبيق وعبر الواجهة البرمجية والأتمتة على حد سواء. وقائمة أنواع الأحداث في فهرس الأحداث.
إعداد نقطة استقبال
يضيف مسؤول مساحة العمل نقاط الاستقبال إلى التكامل من الإعدادات ← التكاملات ← (التكامل) ← خطافات الويب (انظر التكاملات):
- العنوان:
httpsفقط، على مضيف عام. تُرفض العناوين الخاصة وعناوين الاسترجاع والعناوين المحلية عند الحفظ ومع كل إرسال. وقد لا يسمح النشر المخصص إلا بقائمة مضيفين معتمدة (422 forbidden_host)، أو لا يسمح بخطافات الويب إطلاقًا (422 webhook_egress_not_configured). - أنواع الأحداث:
*(كل أحداث الموارد)، أو قائمة مثلemployee.createdوemployee.updatedوapplication.hired. والحدث الاختياريaudit.recordedليس جزءًا من*أبدًا؛ اختره صراحةً (يتطلبaudit.view). - سر التوقيع: يبدأ بـ
whsec_ويليه 64 خانة سداسية عشرية، ويظهر عند إنشاء نقطة الاستقبال، ويستطيع مسؤول مساحة العمل إظهاره لاحقًا (ويُسجَّل كل إظهار في سجل التدقيق). - إرسال حدث تجريبي يرسل حدث
pingللتحقق من العنوان ومن شيفرة التحقق من التوقيع لديك.
لا تتلقى نقطة الاستقبال إلا أحداث الموارد التي يستطيع تكاملها قراءتها، وتُخفى بيانات تواصل المرشح في الحمولة ما لم يكن التكامل يملك recruitment.candidate.contact. ويُخفى الأجر ما لم يكن يملك structure.functional.salary: تكون حقول الأجر في الوظائف والأوصاف الوظيفية null، وتُستبعد حقول الأجر من changedFields، ولا يُرسل التعديل الذي لم يغيّر إلا الأجر، ويستبعد audit.recorded تغييرات الأجر.
الطلب
POST /jadiroon-webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Jadiroon-Webhooks/1
Jadiroon-Event-Id: evt_8fb352d7c1a04e2b9f3a6c10
Jadiroon-Event-Type: employee.updated
Jadiroon-Signature: t=1761300374,v1=5c1f0e…
{
"id": "evt_8fb352d7c1a04e2b9f3a6c10",
"type": "employee.updated",
"apiVersion": "v1",
"occurredAt": "2026-10-24T10:06:14.331Z",
"workspaceId": "cmvikf7rn0001knoq5veyug12",
"actor": { "type": "user", "id": "cmvikf7xk0003knoqj7x054vw", "name": "Sara Alharbi" },
"data": {
"object": { "id": "cmv…", "status": "EMPLOYEE", "firstName": "Omar", "lastName": "Alqahtani", "externalRefs": [{ "system": "oracle-hcm", "externalId": "EMP-1002" }], "…": "…" },
"changedFields": ["currentPositionId"]
}
}
data.objectهو الكائن نفسه الذي تعيده الواجهة البرمجية لهذا المورد، كما كان عند إنشاء الحدث.changedFieldsتسرد الحقول العامة التي تغيرت، حين تكون معروفة.- أحداث الحذف (
<resource>.deleted) لا تحمل إلا{ id, externalRefs }. <resource>.bulk_changedيعني أن صفوفًا كثيرة تغيرت دفعة واحدة (استيراد، أو استبدال هيكل): وdata.objectهو{ resource }. أعد قراءة المورد بـupdatedSince= قيمةoccurredAtفي الحدث، واقرأ سجل المحذوفات.actor.typeقيمتهuserأوintegration(مفتاح واجهة برمجية؛ وnameاسم التكامل) أوsystem(أتمتة).- قد يختلف ترتيب المفاتيح في JSON عن المثال؛ حلّله ولا تقارنه كنص.
التحقق من التوقيع
الترويسة Jadiroon-Signature بصيغة t=<ثواني يونكس>,v1=<HMAC-SHA256 سداسي عشري>، محسوبة بسر نقطة الاستقبال على t + "." + الجسم الخام. تحقق منها دائمًا قبل الوثوق بالحمولة:
- خذ جسم الطلب الخام كما وصل تمامًا (قبل أي تحليل JSON).
- اقرأ
tوكل قيمv1=من الترويسة. - ارفض الطلب إن ابتعد
tعن ساعتك أكثر من 5 دقائق (حماية من إعادة الإرسال). - احسب HMAC-SHA256 لـ
t + "." + bodyبالسر، بخانات سداسية عشرية صغيرة، وقارنه بزمن ثابت مع كل قيمةv1. أي تطابق يكفي.
Node.js
const crypto = require("crypto");
function verifyJadiroonSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = String(header || "").split(",").map((p) => p.trim());
const t = Number((parts.find((p) => p.startsWith("t=")) || "").slice(2));
const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return signatures.some(
(s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);
}
مع Express، اقرأ الجسم خامًا لهذا المسار (express.raw({ type: "application/json" })) ومرّر req.body.toString("utf8").
Python
import hashlib
import hmac
import time
def verify_jadiroon_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = [p.strip() for p in (header or "").split(",")]
t_part = next((p for p in parts if p.startswith("t=")), "")
try:
t = int(t_part[2:])
except ValueError:
return False
if abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1="))
مع Flask مرّر request.get_data()، ومع Django مرّر request.body.
تُوقَّع كل محاولة من جديد بقيمة t جديدة، فلإعادة إرسال الحدث نفسه توقيع جديد.
الرد
- رُدّ بأي حالة 2xx خلال 10 ثوانٍ. ونفّذ العمل الفعلي بعد الرد (ضعه في طابور).
- أي شيء آخر يُعد فشلًا: حالة أخرى، أو انتهاء المهلة، أو خطأ اتصال، أو إعادة توجيه (لا تُتبع إعادة التوجيه).
إعادة المحاولة
يُعاد إرسال التسليم الفاشل وفق هذا الجدول، محسوبًا من المحاولة السابقة (وهو التوقيت حين يعمل مُرسِل إعادة المحاولة كل دقيقة؛ انظر متى تُنفَّذ إعادة المحاولة للخدمة المستضافة):
| المحاولة | التأخير |
|---|---|
| 1 | فورًا |
| 2 | دقيقة واحدة |
| 3 | 5 دقائق |
| 4 | 30 دقيقة |
| 5 | ساعتان |
| 6 | 6 ساعات |
| 7 | 12 ساعة |
| 8 و9 و10 | 24 ساعة لكل منها |
بعد المحاولة العاشرة الفاشلة (نحو 3 أيام و20 ساعة بعد الحدث وفق هذا الجدول) يُعلَّم التسليم ميتًا ولا يُعاد. ويستطيع مسؤول مساحة العمل إعادة إرسال أي حدث من سجل تسليمات نقطة الاستقبال ما دام محفوظًا (30 يومًا)؛ ولإعادة الإرسال معرّف الحدث نفسه. وتُبنى إعادة الإرسال من جديد بصلاحيات التكامل الحالية (وبحالة السجل الحالية)، وتتطلب أن تكون النقطة والتكامل نشطين. وتُرفض بـ 409 conflict إن لم تعد النقطة مشتركة في نوع الحدث (details.reason بقيمة not_subscribed) أو لم يعد السجل موجودًا (resource_gone)، وبـ 403 forbidden (out_of_scope) إن لم يعد التكامل يملك صلاحية رؤيته.
متى تنفذ إعادة المحاولة
تُرسَل المحاولة الأولى فور وقوع التغيير. أما إعادة المحاولة فيرسلها مُرسِل يعمل في الخلفية، والجدول أعلاه هو توقيتها حين يعمل كل دقيقة (كما في نشر مستضاف ذاتيًا يستدعيه من مهمة cron في النظام بـ CRON_SECRET الخاص به). وتشغّل الخدمة المستضافة حاليًا مُرسِل إعادة المحاولة مرة واحدة يوميًا، في الساعة 03:00 بالتوقيت العالمي (UTC). فتُرسَل كل إعادة محاولة في أول تشغيل يومي بعد انقضاء تأخيرها، أي مرة واحدة يوميًا على الأكثر، وتمتد المحاولات العشر 8 أيام أو أكثر بدلًا من 3 أيام و20 ساعة. وفي التشغيل اليومي نفسه يُعاد فحص التسليمات المعلّقة بسبب الإيقاف المؤقت، وتُعطَّل نقاط الاستقبال تلقائيًا، وتُحذف الأحداث القديمة. وإن لم تستطع انتظار التشغيل التالي، فأصلح المستقبِل وأعد الإرسال من سجل التسليمات، أو استدرك بـ updatedSince.
التعطيل التلقائي
عندما تفشل نقطة الاستقبال باستمرار مدة 3 أيام (دون تسليم ناجح منذ أول فشل)، تُعطَّل عند محاولتها الفاشلة التالية (في الخدمة المستضافة عادةً في التشغيل اليومي التالي) ويتلقى مسؤولو مساحة العمل إشعارًا (وفي نقطة الاستقبال على مستوى النشر في لوحة إدارة المنصة، يتلقى كل مالك في لوحة الإدارة رسالة بريد بدلًا من ذلك). ثم تفشل تسليماتها المعلّقة. أصلح المستقبِل، وأعد تفعيل نقطة الاستقبال من الإعدادات (ونقطة الاستقبال على مستوى النشر من لوحة إدارة المنصة ← التكاملات)، وأعد إرسال ما فاتك (أو استدرك بـ updatedSince).
أما الإيقاف المؤقت لنقطة الاستقبال أو لتكاملها فمختلف: لا تتلقى أحداثًا جديدة ما دامت موقوفة (ولا تُحفظ لوقت لاحق)، والتسليمات المعلّقة بالفعل تنتظر ويُعاد إرسالها حين تعود نشطة.
تضييق النطاق
تُجهَّز الرسالة لحظة وقوع الحدث، بالصلاحيات التي كانت للتكامل حينها. لذلك حين يفقد التكامل صلاحية، تُسقط التسليمات المنتظرة على نقاطه: تُعلَّم كل منها فاشلة مع الاستجابة scope changed ولا تُرسل. وقد يُسلَّم مرة واحدة حدثٌ وُزّع في اللحظة نفسها التي حدث فيها التغيير. وكذلك حين تتوقف نقطة استقبال عن الاشتراك في بعض أنواع الأحداث، تُسقط تسليماتها المنتظرة من تلك الأنواع بالطريقة نفسها. الخصوصية مقدّمة على الاكتمال: بعد تضييق النطاق استدرك بـ updatedSince على القوائم التي ما زلت تقرؤها.
الترتيب والتكرار
- الترتيب غير مضمون. إعادة المحاولة والإرسال المتوازي تعني أن حدثًا أقدم قد يصل بعد حدث أحدث. رتّب حسب
occurredAt، وعند تطبيقdata.objectتجاهله إن كانت لديك قيمةupdatedAtأحدث لذلك السجل. - التسليم مرة واحدة على الأقل. قد يصل الحدث نفسه أكثر من مرة (إعادة محاولة بعد انتهاء المهلة، أو إعادة إرسال). أزل التكرار حسب
idالحدث (وهو أيضًا فيJadiroon-Event-Id)، واحتفظ بمعرّفات الأيام القليلة الأخيرة. - تُحفظ الأحداث 30 يومًا. ولما هو أقدم، أو بعد انقطاع طويل، أعد المزامنة بـ
updatedSinceوسجل المحذوفات.
تدوير السر
تدوير السر ينشئ سرًا جديدًا ويُبقي القديم صالحًا 24 ساعة. وفي هذه المدة يحمل كل طلب قيمتين v1= (السر الجديد أولًا)، فيستمر المستقبِل في التحقق بأي من السرين. انشر السر الجديد خلال الـ 24 ساعة.
مدة الحفظ
تُحفظ الأحداث وتسليماتها 30 يومًا، لأن الحمولات تحتوي بيانات شخصية. وتُحفظ آثار الحذف في سجل المحذوفات 180 يومًا.