# إشعارات Webhook

ترسل `DZBuild` طلب `POST` عبر `HTTPS` إلى رابط `webhook` الخاص بتطبيقك عندما يقع حدث في متجر ثبّت التطبيق. كل طلب موقّع بمفتاح التوقيع الخاص بتطبيقك، فيستطيع خادمك التأكد من أنه صادر عن `DZBuild` قبل أن يتصرّف بناءً عليه.

## رابط Webhook[​](#رابط-webhook "رابط مباشر إلى رابط Webhook")

تحدّد رابط `webhook` واحدًا والأحداث التي تريدها لتطبيقك في [منصة المطورين](https://dzbuild.com/dashboard/developer). يستقبل الرابط نفسه أحداث كل المتاجر التي تثبّت التطبيق، ويبيّن الحقل `store_id` في كل حمولة المتجرَ المعني.

يجب أن يحترم الرابط هذه الشروط، وإلا رفضت منصة المطورين حفظه:

* `https` فقط، على المنفذ 443.
* اسم مضيف عام بنطاق من المستوى الأعلى. تُرفض عناوين `IP` وأسماء المستخدمين وكلمات المرور داخل الرابط.
* 255 حرفًا على الأكثر.
* ألا يكون على نطاق تابع لـ `DZBuild` (`dzbuild.app`، `dzbuild.com`، `dzbuild.me`، `dzme.app`، `minacef.com`، `twarc.net`) ولا على `workers.dev`.

القاعدة الأخيرة تعني أن `Worker` على رابط `workers.dev` مجاني لا يستطيع استقبال إشعارات `webhook`. ضعه على نطاق تملكه في حسابك على `Cloudflare` (النطاقات المخصصة متاحة في خطة `Cloudflare` المجانية)، وسجّل ذلك الرابط، ثم تحقق منه.

إن كنت تحتاج الطلبات الجديدة فقط، فاستعلم عنها بدل ذلك: طلب `GET /v1/orders?since=<last created_at>&limit=50` واحد لكل متجر في الدقيقة يبقى ضمن 120 طلبًا في الدقيقة لرمز التثبيت، وهذا ما يفعله قالب [cloudflare-orders](https://github.com/DZBuild-com/dzbuild-app-starter/blob/main/cloudflare-orders/README.md) افتراضيًا. أما تغيّرات الحالة فما زالت تحتاج إلى إشعارات `webhook`.

عند الضغط على «التحقق من الـ `Webhook`» في منصة المطورين، ترسل `DZBuild` طلب `webhook.verify` واحدًا إلى الرابط. أجب عنه بأي رمز حالة `2xx` خلال 6 ثوانٍ. لا يستقبل رابطك أحداثًا حقيقية إلا بعد التحقق منه. والتحقق الناجح يعيد أيضًا تفعيل الإرسال لكل تثبيت عُطّلت نقطة استقباله بعد إخفاقات متكررة.

لا تصل أحداث الطلبات إلى تثبيت إلا إذا منح التاجر صلاحية `orders:read`، لأن الحمولة تتضمن اسم المشتري وهاتفه وعنوانه. من دون هذه الصلاحية لا يستقبل التثبيت أي حدث طلب، بل `app.uninstalled` فقط.

## الأحداث[​](#الأحداث "رابط مباشر إلى الأحداث")

| الحدث              | متى يُرسل                                                 |
| ------------------ | --------------------------------------------------------- |
| `order.created`    | عند تسجيل طلب جديد في المتجر.                             |
| `order.confirmed`  | عند تغيّر حالة الطلب إلى مؤكَّد.                          |
| `order.processing` | عند تغيّر حالة الطلب إلى قيد التجهيز.                     |
| `order.shipped`    | عند تغيّر حالة الطلب إلى مُرسَل.                          |
| `order.delivered`  | عند تغيّر حالة الطلب إلى مُسلَّم.                         |
| `order.cancelled`  | عند تغيّر حالة الطلب إلى ملغى.                            |
| `order.returned`   | عند تغيّر حالة الطلب إلى مُرتجَع.                         |
| `app.uninstalled`  | عندما يزيل التاجر تطبيقك من المتجر. يصلك حتى لو لم تختره. |
| `webhook.verify`   | عند الضغط على «التحقق من الـ `Webhook`» في منصة المطورين. |

تفحص `DZBuild` الطلبات الجديدة وتغيّرات الحالة مرة كل دقيقة. يُرسل `order.created` في أول فحص يرى الطلب الجديد، فتوقّع وصوله خلال دقيقة أو دقيقتين. يُرسل كل حدث حالة مرة واحدة على الأكثر لكل طلب.

## الحمولة[​](#الحمولة "رابط مباشر إلى الحمولة")

جسم كل طلب غلاف `JSON` واحد. مثال الطلب هذا مختصر:

```
{

  "id": "evt_9b2f6c1d04e8a7b35c6d2e10",

  "event": "order.confirmed",

  "created_at": "2026-09-26T14:05:09+01:00",

  "store_id": 1234,

  "data": {

    "order": { "id": 98765, "order_number": "ORD-1234-20260926-1a2b3c4d", "status": "confirmed" },

    "items": [ { "product_id": 555, "product_name": "Sac en cuir", "quantity": 1 } ],

    "previous_status": "pending"

  }

}
```

| الحقل        | المعنى                                                                           |
| ------------ | -------------------------------------------------------------------------------- |
| `id`         | معرّف الحدث. يبقى نفسه في كل إعادة محاولة للحدث نفسه، فاستعمله لإسقاط التكرارات. |
| `event`      | اسم الحدث.                                                                       |
| `created_at` | وقت إنشاء الغلاف، بصيغة `ISO 8601` مع فارق التوقيت عن `UTC`.                     |
| `store_id`   | المتجر الذي يخصّه الحدث. قيمته `0` في `webhook.verify`.                          |
| `data`       | بيانات الحدث.                                                                    |

في أحداث الطلبات يحمل `data.order` هذه الحقول: `id`، `store_id`، `order_number`، `store_seq`، `status`، `payment_status`، `payment_method`، `delivery_type`، `desk_name`، `customer_name`، `customer_phone`، `customer_email`، `wilaya_id`، `wilaya_name` (الاسم بالعربية)، `commune`، `address`، `subtotal`، `shipping_cost`، `discount`، `total`، `promo_code`، `customer_notes`، `tracking_number`، `delivery_company`، `created_at`، `updated_at`. و`data.items` قائمة تضم لكل سطر `product_id` و`variant_id` و`product_name` و`variant_name` و`sku` و`price` و`quantity` و`total` و`source`. وتحمل أحداث الحالة أيضًا `data.previous_status`.

## ترويسات الإرسال[​](#ترويسات-الإرسال "رابط مباشر إلى ترويسات الإرسال")

| الترويسة         | القيمة                                                                                                      |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `Content-Type`   | `application/json`                                                                                          |
| `X-DZ-Timestamp` | توقيت `Unix` بالثواني لحظة التوقيع.                                                                         |
| `X-DZ-Signature` | `t=<timestamp>,v1=<signature>`                                                                              |
| `X-DZ-Event`     | اسم الحدث، وهو نفس `event` في الجسم.                                                                        |
| `X-DZ-Delivery`  | معرّف الإرسال. يبقى نفسه بين محاولات الإرسال الواحد. لا يحمله طلب `webhook.verify` القادم من منصة المطورين. |
| `User-Agent`     | `DZBuild-Webhooks/1.0`                                                                                      |

## التوقيع[​](#التوقيع "رابط مباشر إلى التوقيع")

تتكوّن `X-DZ-Signature` من جزأين تفصل بينهما فاصلة:

* `t` هو الطابع الزمني `Unix`، وهو نفس قيمة `X-DZ-Timestamp`.
* `v1` هو `HMAC-SHA256` بالنظام الست عشري بأحرف صغيرة للسلسلة `t` ثم `.` ثم جسم الطلب كما وصل، ومفتاحه مفتاح التوقيع الخاص بتطبيقك.

للتحقق من طلب:

1. اقرأ بايتات الجسم الخام قبل أي تحليل لـ `JSON`. الجسم الذي حُلّل ثم أعيد تحويله إلى نص لن يطابق التوقيع.
2. احسب `HMAC-SHA256` بمفتاح التوقيع على الطابع الزمني ثم نقطة ثم الجسم الخام.
3. قارن النتيجة بقيمة `v1` مقارنةً ثابتة الزمن.
4. ارفض الطلب إذا ابتعد الطابع الزمني عن ساعتك بأكثر من 5 دقائق. هذا يمنع إعادة إرسال طلب قديم.

يظهر مفتاح التوقيع في منصة المطورين. احفظه على خادمك فقط. عند تجديده، توقّع كل التثبيتات بالمفتاح الجديد ابتداءً من الإرسال الموالي، دون فترة تداخل، فانشر المفتاح الجديد على خادمك في اللحظة نفسها.

### Node.js[​](#nodejs "رابط مباشر إلى Node.js")

```
const crypto = require('crypto');

const express = require('express');



const app = express();

const SECRET = process.env.DZBUILD_SIGNING_SECRET;



function parseSignature(header) {

  const parts = {};

  for (const pair of (header || '').split(',')) {

    const i = pair.indexOf('=');

    if (i > 0) parts[pair.slice(0, i)] = pair.slice(i + 1);

  }

  return parts;

}



app.post('/dzbuild/webhooks', express.raw({ type: 'application/json' }), (req, res) => {

  const { t, v1 } = parseSignature(req.get('X-DZ-Signature'));

  if (!t || !v1 || !/^\d+$/.test(t)) return res.status(401).end();



  const expected = crypto.createHmac('sha256', SECRET).update(t + '.').update(req.body).digest('hex');

  const valid = v1.length === expected.length

    && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));

  if (!valid) return res.status(401).end();

  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(401).end();



  const event = JSON.parse(req.body.toString('utf8'));

  // Queue event for processing, then answer fast.

  res.status(200).end();

});
```

### PHP[​](#php "رابط مباشر إلى PHP")

```
<?php

$secret = getenv('DZBUILD_SIGNING_SECRET');

$raw = file_get_contents('php://input');



$parts = [];

foreach (explode(',', $_SERVER['HTTP_X_DZ_SIGNATURE'] ?? '') as $pair) {

    [$k, $v] = array_pad(explode('=', $pair, 2), 2, '');

    $parts[$k] = $v;

}

$t = $parts['t'] ?? '';

$v1 = $parts['v1'] ?? '';



$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);

if (!ctype_digit($t) || !hash_equals($expected, $v1) || abs(time() - (int) $t) > 300) {

    http_response_code(401);

    exit;

}



$event = json_decode($raw, true);

// Queue $event for processing, then answer fast.

http_response_code(200);
```

### Python[​](#python "رابط مباشر إلى Python")

```
import hashlib

import hmac

import json

import os

import time



from flask import Flask, abort, request



app = Flask(__name__)

SECRET = os.environ["DZBUILD_SIGNING_SECRET"].encode()





@app.post("/dzbuild/webhooks")

def dzbuild_webhook():

    raw = request.get_data()

    parts = dict(

        pair.split("=", 1)

        for pair in request.headers.get("X-DZ-Signature", "").split(",")

        if "=" in pair

    )

    t = parts.get("t", "")

    v1 = parts.get("v1", "")



    expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()

    if not t.isdigit() or not hmac.compare_digest(expected, v1):

        abort(401)

    if abs(time.time() - int(t)) > 300:

        abort(401)



    event = json.loads(raw)

    # Queue event for processing, then answer fast.

    return "", 200
```

## إعادة المحاولة والإرسال المتروك[​](#إعادة-المحاولة-والإرسال-المتروك "رابط مباشر إلى إعادة المحاولة والإرسال المتروك")

ينجح الإرسال عندما يجيب خادمك برمز حالة `2xx` خلال 10 ثوانٍ. أي رمز آخر، أو إعادة توجيه (`3xx`)، أو تجاوز للمهلة، أو خطأ في الاتصال يُحسب محاولةً فاشلة. لا تُتبع إعادات التوجيه.

لكل إرسال 5 محاولات على الأكثر. بعد كل محاولة فاشلة تنتظر المحاولة الموالية على الأقل:

| المحاولة الفاشلة | الانتظار قبل المحاولة الموالية                               |
| ---------------- | ------------------------------------------------------------ |
| 1                | 60 ثانية                                                     |
| 2                | 5 دقائق                                                      |
| 3                | 30 دقيقة                                                     |
| 4                | ساعتان                                                       |
| 5                | لا انتظار. يُعلَّم الإرسال متروكًا ولا يُرسل مرة أخرى أبدًا. |

هذه المدد حدّ أدنى لأن عملية الإرسال تعمل مرة كل دقيقة. الإرسال مضمون مرة واحدة على الأقل: قد يصلك الطلب مرتين، مثلًا بعد عطل في الشبكة من جهة `DZBuild`، فأسقط التكرارات بواسطة `id` الغلاف.

بعد 10 محاولات فاشلة متتالية، مهما كان الإرسال، تُعطَّل نقطة استقبال ذلك التثبيت وتُترك إرسالاته المعلّقة. أصلح خادمك، ثم اضغط «التحقق من الـ `Webhook`» في منصة المطورين لإعادة تفعيل الإرسال. الإرسالات المتروكة من قبل لا يُعاد إرسالها.

تُترك الإرسالات المعلّقة أيضًا دون إرسال عندما توقف `DZBuild` تطبيقك، أو عندما تنزل خطة المتجر تحت الخطة الدنيا لتطبيقك، أو عندما يعمل تطبيق في وضع التجربة على متجر لا يملكه المطوّر. الاستثناء هو `app.uninstalled` الذي يُرسل رغم ذلك.

## app.uninstalled[​](#appuninstalled "رابط مباشر إلى app.uninstalled")

عندما يزيل تاجر تطبيقك، تلغي `DZBuild` رمز التثبيت، وتترك كل الإرسالات المعلّقة لذلك التثبيت، وترسل حدث `app.uninstalled` واحدًا إذا كانت نقطة استقبال التثبيت مفعّلة ومتحقَّقًا منها:

```
{

  "id": "evt_5a0c7e3b9d1f24681ace3579",

  "event": "app.uninstalled",

  "created_at": "2026-09-26T14:05:09+01:00",

  "store_id": 1234,

  "data": {

    "install_id": 57,

    "client_id": "dzapp_4e1b9c07d2a86f35e0b1",

    "store_id": 1234,

    "uninstalled_at": "2026-09-26T13:05:09+00:00"

  }

}
```

الحقل `uninstalled_at` بتوقيت `UTC`. يتبع الإشعار جدول إعادة المحاولة نفسه، وتبقى نقطة الاستقبال مفعّلة يومًا واحدًا بعد الإزالة حتى تكتمل المحاولات. إذا أعاد التاجر تثبيت تطبيقك على ذلك المتجر قبل وصول الإشعار، يُترك الإشعار المعلّق حتى لا يلغي التثبيت الجديد.

بعد الإزالة، تجيب طلبات الواجهة البرمجية برمز التثبيت القديم بـ `401`. تعامل مع بيانات المتجر كما تصفه [قواعد الأمان](https://dzbuild.dev/ar/ar/security.md).

## لا X-DZ-Token للتطبيقات[​](#لا-x-dz-token-للتطبيقات "رابط مباشر إلى لا X-DZ-Token للتطبيقات")

إشعارات `webhook` التي ينشئها التاجر في لوحة تحكم `DZBuild` تحمل أيضًا ترويسة `X-DZ-Token`، وهي نسخة صريحة من السرّ موجّهة لأدوات `no-code`. أما الإرسالات إلى التطبيقات فلا تحملها أبدًا: سرّ توقيع واحد يغطي كل المتاجر التي تثبّت تطبيقك، ونسخة منه في سجل الطلبات تسمح لأي شخص بتزوير أحداث لكل تلك المتاجر. تحقّق دائمًا من `X-DZ-Signature`.
