إشعارات Webhook
ترسل DZBuild طلب POST عبر HTTPS إلى رابط webhook الخاص بتطبيقك عندما يقع حدث في متجر ثبّت التطبيق. كل طلب موقّع بمفتاح التوقيع الخاص بتطبيقك، فيستطيع خادمك التأكد من أنه صادر عن DZBuild قبل أن يتصرّف بناءً عليه.
رابط Webhook
تحدّد رابط webhook واحدًا والأحداث التي تريدها لتطبيقك في منصة المطورين. يستقبل الرابط نفسه أحداث كل المتاجر التي تثبّت التطبيق، ويبيّن الحقل 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 افتراضيًا. أما تغيّرات الحالة فما زالت تحتاج إلى إشعارات 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ثم.ثم جسم الطلب كما وصل، ومفتاحه مفتاح التوقيع الخاص بتطبيقك.
للتحقق من طلب:
- اقرأ بايتات الجسم الخام قبل أي تحليل لـ
JSON. الجسم الذي حُلّل ثم أعيد تحويله إلى نص لن يطابق التوقيع. - احسب
HMAC-SHA256بمفتاح التوقيع على الطابع الزمني ثم نقطة ثم الجسم الخام. - قارن النتيجة بقيمة
v1مقارنةً ثابتة الزمن. - ارفض الطلب إذا ابتعد الطابع الزمني عن ساعتك بأكثر من 5 دقائق. هذا يمنع إعادة إرسال طلب قديم.
يظهر مفتاح التوقيع في منصة المطورين. احفظه على خادمك فقط. عند تجديده، توقّع كل التثبيتات بالمفتاح الجديد ابتداءً من الإرسال الموالي، دون فترة تداخل، فانشر المفتاح الجديد على خادمك في اللحظة نفسها.
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
$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
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
عندما يزيل تاجر تطبيقك، تلغي 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. تعامل مع بيانات المتجر كما تصفه قواعد الأمان.
لا X-DZ-Token للتطبيقات
إشعارات webhook التي ينشئها التاجر في لوحة تحكم DZBuild تحمل أيضًا ترويسة X-DZ-Token، وهي نسخة صريحة من السرّ موجّهة لأدوات no-code. أما الإرسالات إلى التطبيقات فلا تحملها أبدًا: سرّ توقيع واحد يغطي كل المتاجر التي تثبّت تطبيقك، ونسخة منه في سجل الطلبات تسمح لأي شخص بتزوير أحداث لكل تلك المتاجر. تحقّق دائمًا من X-DZ-Signature.