Webhooks

Webhooks الصادرة

ترسل وجهة أحداث JSON إلى عنوانك عند تغيّر بيانات المتجر (POST أو PUT/PATCH/GET). السر الاختياري يوقّع كل تسليم للتحقق من الأصالة.

إدارة نقاط الاستقبال

افتح بوابة التاجر → إعدادات المتجر → المطورون → Webhooks. لكل نقطة:

  • اللقب — تسمية اختيارية تظهر في البوابة.
  • URL — وجهة HTTPS للتسليم.
  • Method — POST (افتراضي)، PUT، PATCH، أو GET.
  • Event — نوع حدث واحد أو أكثر (أو * للكل).
  • Secret — سر مشترك اختياري (٨ أحرف على الأقل) لتوقيع HMAC.

يمكنك أيضًا إنشاء النقاط عبر POST /api/v1/webhooks/endpoints بنطاق webhooks.write.

سلوك التسليم

عند وقوع حدث مشترك، تُدرَج عملية تسليم وتُرسل فورًا:

  • حتى ٦ محاولات إجمالًا (محاولة أولى + ٥ إعادة).
  • تأخيرات الإعادة: ٣٠ث ← ٢د ← ١٠د ← ١س ← ٦س.
  • فشل متواصل لنحو أسبوع يوقف النقطة ويُشعر المتجر.

حدّث الرابط أو أعد التفعيل من المطورون → Webhooks بعد إصلاح المستقبِل.

ترويسات الطلب

كل تسليم يتضمن:

  • Content-Type: application/json
  • User-Agent: Wijha-Webhooks/1.0
  • X-Wijha-Timestamp — مللي ثانية Unix وقت الإرسال
  • X-Wijha-Signature — عند وجود سر فقط: HMAC-SHA256 سداسي لـ `${timestamp}.${rawBody}`
Verify signature (Node.js)
const crypto = require("crypto");

function verifyWijhaSignature({ secret, timestamp, rawBody, signature }) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(signature, "utf8"),
  );
}

// Express example — keep the raw body for HMAC verification
app.post("/webhooks/wijha", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Wijha-Timestamp");
  const signature = req.get("X-Wijha-Signature");
  const rawBody = req.body.toString("utf8");
  if (!verifyWijhaSignature({
    secret: process.env.WIJHA_WEBHOOK_SECRET,
    timestamp,
    rawBody,
    signature,
  })) {
    return res.status(401).send("invalid signature");
  }
  const event = JSON.parse(rawBody);
  // handle event.eventType + event.data / event.changes
  res.status(200).send("ok");
});

JSON التسليم

كل محاولة ترسل هذا الغلاف. data يحوي لقطات الكيانات؛ changes يحوي حقول الحدث المقتضبة.

{
  "id": "del_01JABCXYZ",
  "event": "OrderCreated",
  "eventType": "OrderCreated",
  "storeId": "str_01JSTORE",
  "occurredAt": "2026-10-08T17:12:04.128Z",
  "data": {
    "order": {
      "id": "ord_01JABC",
      "storeId": "str_01JSTORE",
      "number": "10042",
      "channel": "portal",
      "status": "pending",
      "customerId": "cus_01JDEF",
      "customerName": "Ahmed Hassan",
      "customerPhone": "+201012345678",
      "shippingAddress": {
        "city": "Cairo",
        "line1": "12 Nile St",
        "phone": "+201012345678"
      },
      "lineItems": [
        {
          "productId": "prd_01J",
          "variantId": "var_01J",
          "sku": "TEE-BLK-M",
          "title": "Cotton tee",
          "qty": 2,
          "price": 225
        }
      ],
      "payment": { "method": "cod", "codAmount": 495, "currency": "EGP" },
      "totals": {
        "subtotal": 450,
        "shipping": 45,
        "total": 495,
        "currency": "EGP"
      },
      "createdAt": "2026-10-08T17:12:04.000Z"
    },
    "customer": {
      "id": "cus_01JDEF",
      "name": "Ahmed Hassan",
      "phone": "+201012345678",
      "email": "ahmed@example.com"
    }
  },
  "changes": {
    "aggregateType": "order",
    "aggregateId": "ord_01JABC"
  }
}

تظهر فقط المفاتيح ذات الصلة في data لكل حدث. أرجع HTTP 2xx للتأكيد.

الإثراء

قبل الإدراج، توسّع وجهة حدث النطاق المقتضب:

  • id / event / eventType / storeId / occurredAt — بيانات التسليم.
  • data — لقطات كاملة (طلب، شحنة، منتج، عميل، مجموعة، مستوى مخزون حسب الحدث).
  • changes — الحقائق الأصلية (aggregateType، aggregateId، تحولات الحالة، الإجراءات، الأخطاء).

إن تعذّر تحميل كيان مرتبط يُحذف مفتاحه من data؛ changes تصل دائمًا.

كتالوج الأحداث

اشترك في أي من أنواع الأحداث التالية:

  • OrderCreated — طلب جديد
  • OrderUpdated — تغيّرت حقول الطلب
  • OrderStatusChanged — انتقال حالة الطلب
  • ProductUpdated — إنشاء/تحديث/حذف منتج
  • CollectionUpdated — إنشاء/تحديث/حذف مجموعة
  • CustomerCreated — عميل جديد
  • CustomerUpdated — تغيّرت حقول العميل
  • ShipmentCreated — حُجزت شحنة
  • ShipmentDelivered — وُسِمت الشحنة كمُسلَّمة
  • ShipmentCreateFailed — فشل الحجز
  • InventoryAdjusted — تعديل مستوى المخزون
  • StockReceived — استلام مخزون وارد

مثال OrderCreated

لقطة كاملة للطلب مع العميل المرتبط عند التوفر:

{
  "id": "del_01JABCXYZ",
  "event": "OrderCreated",
  "eventType": "OrderCreated",
  "storeId": "str_01JSTORE",
  "occurredAt": "2026-10-08T17:12:04.128Z",
  "data": {
    "order": {
      "id": "ord_01JABC",
      "storeId": "str_01JSTORE",
      "number": "10042",
      "channel": "portal",
      "status": "pending",
      "customerId": "cus_01JDEF",
      "customerName": "Ahmed Hassan",
      "customerPhone": "+201012345678",
      "shippingAddress": {
        "city": "Cairo",
        "line1": "12 Nile St",
        "phone": "+201012345678"
      },
      "lineItems": [
        {
          "productId": "prd_01J",
          "variantId": "var_01J",
          "sku": "TEE-BLK-M",
          "title": "Cotton tee",
          "qty": 2,
          "price": 225
        }
      ],
      "payment": { "method": "cod", "codAmount": 495, "currency": "EGP" },
      "totals": {
        "subtotal": 450,
        "shipping": 45,
        "total": 495,
        "currency": "EGP"
      },
      "createdAt": "2026-10-08T17:12:04.000Z"
    },
    "customer": {
      "id": "cus_01JDEF",
      "name": "Ahmed Hassan",
      "phone": "+201012345678",
      "email": "ahmed@example.com"
    }
  },
  "changes": {
    "aggregateType": "order",
    "aggregateId": "ord_01JABC"
  }
}

مثال ProductUpdated

{
  "id": "del_01JPROD",
  "event": "ProductUpdated",
  "eventType": "ProductUpdated",
  "storeId": "str_01JSTORE",
  "occurredAt": "2026-10-08T16:40:00.000Z",
  "data": {
    "product": {
      "id": "prd_01J",
      "title": "Cotton tee",
      "status": "active",
      "price": 225,
      "currency": "EGP",
      "sku": "TEE-BLK-M"
    }
  },
  "changes": {
    "aggregateType": "product",
    "aggregateId": "prd_01J",
    "action": "updated"
  }
}

مثال CustomerCreated

{
  "id": "del_01JCUS",
  "event": "CustomerCreated",
  "eventType": "CustomerCreated",
  "storeId": "str_01JSTORE",
  "occurredAt": "2026-10-08T15:05:22.000Z",
  "data": {
    "customer": {
      "id": "cus_01JDEF",
      "name": "Ahmed Hassan",
      "phone": "+201012345678",
      "email": "ahmed@example.com",
      "createdAt": "2026-10-08T15:05:22.000Z"
    }
  },
  "changes": {
    "aggregateType": "customer",
    "aggregateId": "cus_01JDEF"
  }
}

مثال ShipmentCreated

{
  "id": "del_01JSHIP",
  "event": "ShipmentCreated",
  "eventType": "ShipmentCreated",
  "storeId": "str_01JSTORE",
  "occurredAt": "2026-10-08T18:20:11.000Z",
  "data": {
    "shipment": {
      "id": "shp_01J",
      "orderId": "ord_01JABC",
      "trackingNumber": "WJH123456789",
      "status": "booked",
      "carrier": "mega"
    },
    "order": {
      "id": "ord_01JABC",
      "number": "10042",
      "status": "waiting_for_pickup"
    }
  },
  "changes": {
    "aggregateType": "shipment",
    "aggregateId": "shp_01J",
    "orderId": "ord_01JABC"
  }
}

الاستجابات المتوقعة

يجب أن يؤكد مستقبلك بسرعة:

  • 2xx — يُعلَّم التسليم ناجحًا؛ تُصفَّر نافذة الفشل المتواصل.
  • حالة أخرى أو مهلة — تُحسب فشلًا؛ تُعاد المحاولة حسب الجدول أعلاه.
  • تسليمات GET ترسل نفس الترويسات؛ يُحذف الجسم في GET.
HTTP/1.1 200 OK
Content-Type: text/plain

ok