تخطَّ إلى المحتوى

Webhooks

الغلاف، والتحقق من التوقيع بأربع لغات، ودلالات التسليم وإعادة المحاولة، والمواضيع.

آخر تحديث 2 أيلول 2026قراءة 4 د
في هذه الصفحة

هذه الصفحة لمن يستقبل أحداث المتاجر. في نهايتها لديك نقطة استقبال تتحقّق من التوقيع، وتزيل التكرار، وترتّب الأحداث، وتعرف بالضبط ما يحدث عند الفشل.

كل حدث في متجر مثبَّت عليه تطبيقك يصل كطلب POST موقّع إلى نقطة الاستقبال الوحيدة لذلك التثبيت. التسليم مرة واحدة على الأقل؛ عالِج التكرار دائماً.

الغلاف#

webhook-envelope.jsonJSON
{
  "id": "9b2f6c1e-7d3a-4f0b-9a52-1c8e2d4b6a70",
  "api_version": "v1",
  "topic": "order.created",
  "store_id": "3065a1f2-0c4d-4e8b-b7a9-5d2f8c1e9a44",
  "install_id": "8b2c1d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d",
  "sequence": 1661,
  "occurred_at": "2026-08-18T16:00:00.123Z",
  "data": {
    "order_id": "6f1d2c3b-4a59-4e8f-9b70-2d1c3e4f5a6b",
    "order_number": "1042",
    "status": "placed",
    "total_minor": 275000,
    "currency": "SYP"
  }
}
الحقل المعنى
id معرّف الحدث الثابت. مفتاح إزالة التكرار عندك؛ لا يتغيّر بين المحاولات.
api_version دائماً v1 (العقد). المراجعة المؤرّخة في الترويسة X-Dukkan-Api-Version.
topic الموضوع؛ الجدول في المواضيع.
store_id المتجر الذي وقع فيه الحدث. ارفض قيمة تختلف عن المتجر الذي تحتفظ به لـ install_id.
install_id التثبيت الذي وُزّع إليه هذا التسليم. ابحث عن سر التوقيع به، لا بمقطع مسار اخترته أنت. داخل التوقيع.
sequence رقم تسلسلي رتيب لكل متجر مع فجوات مسموحة. رتّب به الأحداث المتأخرة، ولا تعتمد على تتاليه.
occurred_at بصيغة RFC 3339 دائماً.
test موجود بقيمة true فقط في تجربة على متجر تجريبي أُطلقت عبر POST /webhooks/test؛ غائب في حركة الإنتاج.
data حمولة الموضوع؛ الأشكال في مرجع الأحداث.

ملاحظة

تحمل حمولات الطلب المبلغ كـ total_minor مع currency دون decimals. اجلب الطلب عبر GET /orders/{id} عندما تحتاج إلى الأس العشري للعرض؛ راجع المال بالوحدات الصغرى.

الترويسات#

الترويسة مثال المعنى
X-Dukkan-Delivery-Id 7d0c2b1a-5e4f-4a3b-8c9d-0e1f2a3b4c5d معرّف التسليم؛ ثابت عبر محاولات التسليم نفسه
X-Dukkan-Install-Id 8b2c1d4e-5f60-4a7b-8c9d-0e1f2a3b4c5d نسخة غير موقّعة من install_id في الغلاف، لتحميل السر قبل التحقق
X-Dukkan-Event order.created الموضوع نفسه الذي في الغلاف
X-Dukkan-Timestamp 1787150000 ثوانٍ Unix لحظة الإرسال
X-Dukkan-Api-Version 2026-09-09 المراجعة المؤرّخة من v1 التي أنتجت الحمولة
X-Dukkan-Hmac-Sha256 3f9a… (hex) التوقيع

التحقق من التوقيع#

التوقيع هو HMAC-SHA256 بمفتاحك dk_whsec_… على السلسلة timestamp.body: الطابع الزمني، ثم نقطة، ثم بايتات الجسم الخام كما وصلت. لا تُعِد تسلسل JSON قبل التحقق. ارفض الطوابع الزمنية الأقدم من 5 دقائق (حماية من إعادة الإرسال)، وقارن بوقت ثابت.

TypeScript
import { verifyWebhookSignature } from "@dukkan.one/app-sdk/webhooks";

const verdict = await verifyWebhookSignature({
  rawBody,                                  // the request bytes, never re-serialised
  timestamp: headers.get("x-dukkan-timestamp"),
  signature: headers.get("x-dukkan-hmac-sha256"),
  secret,                                   // or [current, previous] during a rotation
});
if (!verdict.ok) return new Response(JSON.stringify({ error: verdict.reason }), { status: 401 });

التسليم وإعادة المحاولة#

المرجع الرسمي لهذه الدلالات هو وصف webhooks.storeEvent في مواصفة الواجهة: «أجب بـ 2xx خلال المهلة؛ وإلا تُعاد المحاولة بتراجع أسي حتى 8 محاولات، ثم يُعلَّم التسليم ميتاً، ويؤدي تكرار الفشل إلى تعطيل الاشتراك». الأرقام التالية هي ما تنفّذه منصّة التجّار اليوم:

البند القيمة
المهلة لكل محاولة 10 ثوانٍ
النجاح أي 2xx؛ الجسم يُهمل
جدول إعادة المحاولة تراجع أسي يبدأ بدقيقة ويتضاعف حتى سقف ساعة: 1، 2، 4، 8، 16، 32، 60، 60 دقيقة
عدد المحاولات لكل تسليم 8، ثم يصبح التسليم dead
تعطيل الاشتراك عند 8 إخفاقات متتالية على الأقل واستمرار الفشل 24 ساعة على الأقل معاً؛ لا يكفي أحدهما
أثناء التعطيل تستمر الأحداث بالتراكم لهذا الاشتراك ولا تُفقد؛ يتوقف الإرسال فقط
إعادة التفعيل أعد PUT /webhooks؛ تُسلَّم الأحداث المتراكمة
إعادة الإرسال يدوياً من صفحة التطبيق في البوابة، محاولة واحدة إضافية لكل تسليم ميت
الترتيب تسليم واحد قيد الإرسال لكل تثبيت في الوقت نفسه؛ التسليم البطيء يؤخّر ما بعده في المتجر نفسه فقط
الاحتفاظ سجلات التسليم تُحذف بعد 30 يوماً

نمط المعالج الآمن للتكرار#

  1. تحقّق من التوقيع، ثم أجب 2xx فوراً وعالِج في الخلفية؛ المعالجة الطويلة تُحتسب مهلة.
  2. سجّل id في مخزن فريد قبل المعالجة؛ التعارض يعني تكراراً فتجاهله.
  3. خزّن آخر sequence معالَج لكل store_id؛ حدث بتسلسل أصغر من المخزّن وصل متأخراً، وحدث بتسلسل أكبر بفجوة أمر طبيعي.
  4. اجلب الحالة الحالية من الواجهة عند الشك بدلاً من الاعتماد على ترتيب الوصول.

طابِق ليلياً#

Webhooks هي المسار السريع، لا المسار الوحيد. مرة في اليوم، مرّ على كل ما تغيّر منذ آخر علامة مياه مرتفعة لديك وحدّثه؛ فالتسليم الذي انتهى في صندوق الرسائل الميتة أو المعالج الذي فشل بصمت يُصحَّح خلال يوم بدلاً من أبداً:

TypeScript
import { syncOrders } from "@dukkan.one/app-sdk/commerce";

const result = await syncOrders(client, {
  since: lastHighWaterMark,
  onPage: async (orders) => { for (const order of orders) await upsert(order); },
});
await save(result.highWaterMark);

updated_at_min على نقاط نهاية القوائم هو الأساس؛ ويضيف مساعد الحزمة تداخلاً لمدة دقيقة كي لا يُتجاوز أبداً سجل حُدّث أثناء المرور السابق.

المواضيع#

الموضوع النطاق المطلوب متى
order.created orders:read إنشاء طلب من أي مصدر
order.status_changed orders:read كل انتقال في حالة الطلب
order.paid orders:read عندما تعبر المبالغ المحصَّلة فعلاً إجمالي الطلب في دفتر المدفوعات
fulfillment.requested orders:read إنشاء شحنة بحالة pending: طلب حجز مندوب
fulfillment.created orders:read إنشاء أي شحنة
fulfillment.updated orders:read تغيّر حالة الشحنة أو رقم تتبعها
refund.created orders:read تسجيل استرداد
product.created، product.updated، product.deleted products:read تغيّر منتج أو أحد متغيراته
inventory.movement_created inventory:read حركة مخزون جديدة
app.uninstalled بلا نطاق إلغاء التثبيت؛ يصل حتى بعد إلغاء الرموز

الحمولة data لكل موضوع في مرجع الأحداث. order.paid مستقل تماماً عن التسليم؛ راجع دورة حياة الدفع عند الاستلام.

إدارة الاشتراك#

العملية الأثر
GET /webhooks الاشتراك الحالي أو null؛ لا يعيد المفتاح أبداً
PUT /webhooks ينشئ الاشتراك أو يستبدله بالكامل (قائمة المواضيع كلها)؛ rotate_secret: true يصدر مفتاحاً جديداً يظهر مرة واحدة
DELETE /webhooks يوقف التسليم لهذا التثبيت

يجب أن يكون endpoint_url رابط HTTPS علنياً؛ تُرفض العناوين الخاصة والمحلية. يتطلب الاشتراك في موضوع النطاقَ الذي يحكمه، وإلا 403.