العقد الذي يُكتب عنوانك على أساسه: ماذا نرسل، وكيف يُوقَّع، وكم لديك من وقت للرد، وماذا يحدث إن لم ترد.
يرسل حيّز الحدث إلى عنوانك فور وقوعه: انتهى فحص عنصر بيانات، أصبحت طبقة جاهزة، فشل تشغيل مجدول. هذه الصفحة هي العقد الذي تكتب مستقبِلك على أساسه.
طلب POST بجسم JSON وهذه الترويسات:
| الترويسة | ما تحمله |
|---|---|
X-Haiyz-Signature |
t=<ثوانٍ يونكس>,v1=<hex> — انظر أدناه |
X-Haiyz-Event-Id |
الحدث. ثابت عبر كل إعادة إرسال للحدث نفسه |
X-Haiyz-Event-Type |
الاسم، مثل dataset.layer.ready |
X-Haiyz-Event-Version |
إصدار الحمولة لهذا الاسم |
X-Haiyz-Delivery-Id |
هذا الإرسال. إعادة المحاولة تبقيه، وإعادة التشغيل تأخذ واحدًا جديدًا |
X-Haiyz-Delivery-Attempt |
1 للإرسال الأول، ثم 2 و3… |
X-Haiyz-Environment |
live، أو test لإرسال طلبته من لوحة التحكم |
الجسم هو الغلاف:
{
"id": "evt_01J…",
"type": "dataset.layer.ready",
"version": 1,
"environment": "live",
"createdAt": "2026-09-20T09:14:02.441Z",
"deliveryId": "dlv_01J…",
"data": {
"subject": { "type": "dataset", "id": "66f…" },
"orgId": "66a…"
}
}
الحقلان subject وorgId هما قول الغلاف نفسه عن الحدث، ويغلبان دائمًا أي حقل
بالاسم نفسه داخل حمولة الحدث. وما عدا ذلك تحت data يخصّ نوع الحدث.
التوقيع هو HMAC للطابع الزمني وجسم الطلب الخام كما وصل تمامًا. اقرأه قبل أن يلمسه محلل JSON لديك: إعادة تسلسل الكائن تغيّر البايتات ولن يطابق التوقيع.
signed = "<t>.<الجسم الخام>"
v1 = hex( hmac_sha256(secret, signed) )
قارن في زمن ثابت، ثم تحقق أن t ضمن خمس دقائق من الآن. كلا الشرطين مهم:
بغير فحص الطابع الزمني يمكن إعادة إرسال جسم ملتقَط في أي وقت لاحق، والطابع
الزمني داخل المادة الموقَّعة تحديدًا كي لا يُستبدل بآخر جديد.
import crypto from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=").map((s) => s.trim())),
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return header
.split(",")
.filter((p) => p.trim().startsWith("v1="))
.some((p) =>
crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(p.trim().slice(3), "hex"),
),
);
}
قد تحمل الترويسة أكثر من v1= واحد، وهذا يعني أن تدوير المفتاح جارٍ: المفتاح
القديم والجديد يوقّعان معًا طوال مهلة السماح، فاقبل الطلب إذا طابق أيٌّ منهما.
ردّ بـ 2xx وردّ بسرعة. الإرسال كله — من تحليل النطاق والاتصال وتأمين النقل حتى
ردك — لديه عشر ثوانٍ، وما هو أبطأ يُعدّ فشلًا بقدر ما نستطيع أن نعرف. أقرّ
بالاستلام أولًا ثم نفّذ العمل بعد ذلك.
كل ما ليس 2xx يُعاد إرساله على هذا المنحنى، ابتداءً من أول فشل:
فورًا · 30 ثانية · دقيقتان · 10 دقائق · ساعة · 3 ساعات · 6 ساعات · 12 ساعة
بعد المحاولة الأخيرة يُستنفد الإرسال ويُهمل. والعنوان الذي يفشل دون نجاح واحد
طوال 24 ساعة يُوقَف تلقائيًا ويُعلَّم auto_disabled في لوحة التحكم مع السبب.
وما دام موقوفًا فإن أحداثه لا تُدرج له أصلًا — تضيع ولا تنتظر — لذا يستحق العنوان
الموقوف تلقائيًا تنبيهًا عندك.
قد يصل الحدث نفسه أكثر من مرة: إعادة محاولة بعد ضياع ردك في الطريق، أو إعادة
تشغيل طلبها أحدهم. الترويسة X-Haiyz-Event-Id ثابتة في كل هذه الحالات، فسجّل ما
عالجته من معرّفات وتجاهل ما سبق أن رأيته. افترض التسليم مرة واحدة على الأقل، لا
مرة واحدة بالضبط.
والترتيب غير مضمون كذلك: قد يصل حدثان عن الكائن نفسه بترتيب مقلوب، فاعتمد
createdAt لا وقت الوصول.
سجّله في لوحة التحكم، ضمن المؤسسة › روابط الويب هوك. يجب أن يكون https على
مضيف عام؛ والعنوان الخاص أو المحلي يُرفض عند الحفظ مع بيان السبب.
يُعرض مفتاح التوقيع مرة واحدة، عند إنشاء العنوان وعند التدوير. ولا يستطيع حيّز عرضه ثانية؛ فالمفتاح الذي يقدر الخادم على قراءته مفتاح أقل قيمة.
وفي الشاشة نفسها سجل الإرسال — كل محاولة برمز حالتها ومدتها وخطئها — وزر اختبار يرسل طلبًا موقَّعًا حقيقيًا ويخبرك بما ردّ به عنوانك. استخدمه قبل أن تبحث في أي مكان آخر.