# تسجيل webhook

لا يفرض API v1 حدًّا لعدد webhooks لكل متجر — سجّل ما يحتاجه تكاملك فعليًا من روابط، ونظّف ما توقفت عن استعماله. (أما [إضافة Webhooks للتاجر](https://dzbuild.com/ar/ar/api-docs/webhooks/overview.md) بدون كود فتفرض حدًّا: نقطة واحدة في Unlimited، و3 في Enterprise.)

كل webhook يمكن أن يشترك بقائمة مختلفة من الأحداث؛ اختر النموذج المناسب لك:

* **نقطة واحدة، كل الأحداث** — أسهل للتطبيقات الصغيرة. تفرّع على `event` في معالجك.
* **عدة نقاط، حدث واحد لكل** — أنظف في إعدادات microservices، لكن روابط أكثر للإدارة.

الوصول التجريبي (Pilot)

تحتاج webhooks الخاصة بـ API v1 إلى مفتاح API خاص بالتاجر من متجر على خطة **Enterprise** سارية؛ وأي خطة أخرى، أو اشتراك منتهي الصلاحية، يتلقى `403 forbidden` `"API access requires an active Enterprise plan"`. المفاتيح المُنشأة من `/dashboard/api` مُسجَّلة مسبقًا في البرنامج التجريبي؛ والمفتاح غير المُسجَّل يتلقى `403 forbidden` `"API is in pilot mode; key not enrolled"`. أما رموز التطبيقات المثبّتة فتتلقى `403 forbidden` `"Apps cannot use this endpoint"` على كل نداء تحت `/v1/webhooks`.

## `POST /v1/webhooks` — تسجيل[​](#post-v1webhooks--تسجيل "رابط مباشر إلى post-v1webhooks--تسجيل")

**المصادقة:** مفتاح منصة بصلاحية `webhooks:write`. **يتطلب `Idempotency-Key`.** على خلاف أغلب عمليات الكتابة، لا تُخزَّن هذه الاستجابة لإعادة تشغيلها لأنها تحمل السر: إعادة المحاولة بالمفتاح نفسه تسجّل webhook ثانيًا بسر جديد. إن انتهت مهلة النداء، راجع `GET /v1/webhooks` قبل إعادة المحاولة واحذف أي webhook مكرّر.

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

| الحقل    | النوع              | إلزامي | ملاحظات                                                                                                                                |
| -------- | ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `url`    | string (https URL) | ✅     | يجب أن يكون `https://`؛ ويُرفض الرابط `http://`. الطول الأقصى 500 حرف، والرابط الأطول يُرفض بـ `500 server_error` لا كخطأ تحقق.        |
| `events` | string\[]          | ✅     | قائمة أسماء أحداث. انظر [كتالوج الأحداث](https://dzbuild.com/ar/ar/api-docs/webhooks/events.md) للقيم المسموح بها. مصفوفة فارغة = خطأ. |

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

```
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: $(uuidgen)" \

  -d '{

    "url":    "https://yourapp.example/webhooks/dzbuild",

    "events": ["order.created", "order.confirmed", "order.shipped",

               "order.cancelled", "signup.counted"]

  }'
```

### الاستجابة 200[​](#الاستجابة-200 "رابط مباشر إلى الاستجابة 200")

```
{

  "data": {

    "id":     17,

    "secret": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",

    "note":   "Save the secret now — it is not retrievable after this response."

  },

  "meta": { "request_id": "...", "api_version": "v1" }

}
```

لا تتفرّع أبدًا على `== 201`

نقاط الإنشاء `/v1/webhooks` و `/v1/products` و `/v1/orders` و `/v1/landing-pages` و `/v1/keys` تُجيب بـ **200** عند النجاح، بينما تُجيب نقاط أخرى، مثل `/v1/categories`، بـ **201**. تفرّع على أي 2xx بدلًا من ذلك.

**`secret` يظهر مرة واحدة.** احفظه بجانب معرّف الـ webhook في مدير الأسرار، فكل تسليم إلى هذا الـ webhook يُوقَّع به (انظر [التوقيع](https://dzbuild.com/ar/ar/api-docs/webhooks/overview.md#%D8%A7%D9%84%D8%AA%D9%88%D9%82%D9%8A%D8%B9)). إن فقدته، احذف الـ webhook وأنشئ غيره.

### الأخطاء[​](#الأخطاء "رابط مباشر إلى الأخطاء")

| HTTP | الكود / الرسالة                                                         | السبب                                                                                                  |
| ---- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 400  | `bad_request` "url must be a valid http(s) URL"                         | شكل رابط خاطئ                                                                                          |
| 400  | `bad_request` "url must be https"                                       | رابط يبدأ بـ `http://`                                                                                 |
| 400  | `bad_request` "url host must be a hostname, not an IP literal"          | عنوان IP بدل اسم مضيف                                                                                  |
| 400  | `bad_request` "url host is missing or carries credentials"              | جزء `user:password@` في الرابط                                                                         |
| 400  | `bad_request` "url host must be a public hostname"                      | مضيف ليس اسم نطاق كاملًا، مثل `localhost`                                                              |
| 400  | `bad_request` "url host does not resolve"                               | لا يملك اسم المضيف سجل A أو AAAA                                                                       |
| 400  | `bad_request` "url must resolve to a public address"                    | يشير المضيف إلى عنوان خاص أو محجوز واحد على الأقل                                                      |
| 400  | `bad_request` "url port must be 80 or 443"                              | منفذ مخصّص مثل `:8443`                                                                                 |
| 400  | `bad_request` "Body must be valid JSON"                                 | جسم الطلب ليس JSON صالحًا                                                                              |
| 400  | `bad_request` "events must be a non-empty list"                         | مصفوفة فارغة                                                                                           |
| 400  | `bad_request` "unknown event: foo. Allowed: …"                          | اسم حدث غير موجود في الكتالوج                                                                          |
| 400  | `bad_request` "Idempotency-Key header is required for write requests"   | نسيان `Idempotency-Key` في POST/DELETE                                                                 |
| 400  | `bad_request` "Idempotency-Key must be `<=64` chars, `[A-Za-z0-9_-:.]`" | المفتاح أطول من اللازم، أو يحوي محارف خارج تلك المجموعة (محارف base64 وهي `+` و `/` و `=` كلها مرفوضة) |
| 403  | `forbidden` "API is in pilot mode; key not enrolled"                    | المفتاح موجود لكنه غير مُسجَّل في البرنامج التجريبي                                                    |
| 403  | `forbidden` "API access requires an active Enterprise plan"             | المتجر ليس على خطة Enterprise سارية                                                                    |
| 403  | `forbidden` "Apps cannot use this endpoint"                             | الرمز تابع لتطبيق مثبّت                                                                                |
| 403  | `forbidden` "Missing scope: webhooks<!-- -->:write<!-- -->"             | المفتاح لا يحمل `webhooks:write`                                                                       |
| 500  | `server_error` "Could not register webhook"                             | غالبًا `url` أطول من 500 حرف                                                                           |

## `GET /v1/webhooks` — قائمة[​](#get-v1webhooks--قائمة "رابط مباشر إلى get-v1webhooks--قائمة")

**المصادقة:** مفتاح منصة بصلاحية `webhooks:read`.

```
curl https://api.dzbuild.app/v1/webhooks \

  -H "Authorization: Bearer $DZ_KEY"
```

```
{

  "data": {

    "items": [

      {

        "id":              17,

        "url":             "https://yourapp.example/webhooks/dzbuild",

        "events":          ["order.created", "order.confirmed"],

        "status":          "active",

        "last_success_at": "2026-04-30 21:18:23",

        "last_failure_at": null,

        "failure_count":   0,

        "created_at":      "2026-04-30 19:00:00"

      }

    ],

    "allowed_events": [

      "order.created", "order.confirmed", "order.shipped", "order.delivered",

      "order.cancelled", "order.returned", "payment.received",

      "signup.counted", "event.recorded", "product.stock_low"

    ]

  }

}
```

`allowed_events` هي القائمة التي يقبلها الـ API عند التسجيل — لكنها أوسع مما يُطلق فعليًا. فـ `payment.received` و `event.recorded` و `product.stock_low` تُقبل ثم لا تُصدَر أبدًا. راجع [كتالوج الأحداث](https://dzbuild.com/ar/ar/api-docs/webhooks/events.md) قبل أن تبني على أي منها.

| الحالة   | المعنى                                                                                                                                                |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `active` | يستلم التسليمات. عمليًا هذه هي القيمة الوحيدة التي ستراها.                                                                                            |
| `paused` | محجوزة للاستعمال مستقبلًا، غير مستعملة حاليًا — ولا توجد صفحة في لوحة التحكم لـ webhooks الخاصة بـ API v1.                                            |
| `dead`   | محجوزة للاستعمال مستقبلًا، غير مستعملة حاليًا. لا يوجد تعطيل تلقائي؛ يواصل `failure_count` عدّ المحاولات الفاشلة فقط ويعود إلى `0` عند النجاح التالي. |

لإيقاف التسليمات، احذف الـ webhook.

## `POST /v1/webhooks/{id}/test`[​](#post-v1webhooksidtest "رابط مباشر إلى post-v1webhooksidtest")

أطلق تسليم `webhook.test` لتتحقق من أن نقطتك قابلة للوصول ولترى شكل المظروف.

لا يمكن الاشتراك في `webhook.test`: وضعه في مصفوفة `events` عند التسجيل يُرجع `400 bad_request` `"unknown event: webhook.test. Allowed: …"`. ويُرسَل تسليم الاختبار إلى الـ webhook المستهدف بغض النظر عمّا يشترك فيه ذلك الـ webhook.

**المصادقة:** مفتاح منصة بصلاحية `webhooks:write`. **يتطلب `Idempotency-Key`.**

```
curl -X POST 'https://api.dzbuild.app/v1/webhooks/17/test' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: test-17-$(date +%s)"
```

```
{ "data": { "tested": true, "note": "a webhook.test delivery was enqueued; check your endpoint" } }
```

تسليم الاختبار يبدو هكذا:

```
{

  "event":       "webhook.test",

  "store_id":    13,

  "occurred_at": "2026-04-30T21:24:17+00:00",

  "data":        { "ts": 1717112657 },

  "delivery_id": "9f2c41ab77e05d18"

}
```

أمران يجب معرفتهما عنه:

* **يُوضع في طابور، وليس متزامنًا.** الاستجابة تؤكد فقط أن التسليم دخل الطابور؛ ويصل الـ POST بعد ذلك بقليل، خلال دقيقة عادةً.
* **يختبر شيفرة التحقق لديك.** يُوقَّع بسر `secret` الخاص بـ webhook لديك مثل أي تسليم آخر، فإذا اجتاز فحصك فهو يثبت إمكانية الوصول وشكل الحمولة وصحة التحقق من التوقيع. انظر [التوقيع](https://dzbuild.com/ar/ar/api-docs/webhooks/overview.md#%D8%A7%D9%84%D8%AA%D9%88%D9%82%D9%8A%D8%B9).

## `DELETE /v1/webhooks/{id}`[​](#delete-v1webhooksid "رابط مباشر إلى delete-v1webhooksid")

**المصادقة:** مفتاح منصة بصلاحية `webhooks:write`. **يتطلب `Idempotency-Key`.**

```
curl -X DELETE 'https://api.dzbuild.app/v1/webhooks/17' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: del-17"
```

```
{ "data": { "deleted": true, "id": 17 } }
```

بعد الحذف:

* لا تُجدوَل تسليمات جديدة.
* **كل تسليم في الطابور لذلك الـ webhook يُلغى فورًا.** لا شيء "يُحاوَل مرة أخيرة". إن كان المتراكم يهمك، أوقف كتاباتك ودع الطابور يفرغ (أقل من دقيقة عادةً) قبل الحذف.
* سر الـ webhook صار عديم الفائدة.

### إعادة تشغيل `Idempotency-Key` في `test` و `DELETE`[​](#إعادة-تشغيل-idempotency-key-في-test-و-delete "رابط مباشر إلى إعادة-تشغيل-idempotency-key-في-test-و-delete")

يُخزّن الـ API استجابة كل `Idempotency-Key` لمدة **24 ساعة** ويعيد تشغيلها مع ترويسة `Idempotency-Replay: 1`. لذلك نتيجتان:

* إعادة استعمال مفتاح حرفي مثل `del-17` خلال 24 ساعة **تعيد الاستجابة المخزّنة بدل تنفيذ نداء جديد**. استعمل مفتاحًا جديدًا (أو مفتاحًا يتضمّن رقم المحاولة) كلما أردت فعلًا أن تُنفَّذ العملية.
* استجابات الخطأ تُخزَّن أيضًا. الكتابة الفاشلة تعيد نفس الـ 4xx لمدة 24 ساعة تحت نفس المفتاح — غيّر المفتاح بعد إصلاح الطلب.

إعادة التشغيل مضمونة لمدة 24 ساعة أيًّا كان المضيف الذي تناديه، وتُجيب دائمًا بترويسة `Idempotency-Replay: 1`. ومع ذلك يُفضَّل استعمال `https://api.dzbuild.app/v1`: فالمسار `dzbuild.com/api/v1` مجرد اسم بديل، وقد تُحجب عليه بعض المسارات.

## متطلبات النقطة[​](#متطلبات-النقطة "رابط مباشر إلى متطلبات النقطة")

رابط webhook يجب أن:

* يردّ بـ **2xx** عند النجاح. يُعاد إرسال 5xx و`408` و`429` بعد دقيقة، ثم 5 دقائق، ثم 30 دقيقة، ثم ساعتين، ثم يُسقط؛ أما أي 4xx آخر، وكل 3xx، فينقل التسليم فورًا إلى قائمة الرسائل الميتة (انظر [سلوك إعادة المحاولة](https://dzbuild.com/ar/ar/api-docs/webhooks/overview.md#%D8%B3%D9%84%D9%88%D9%83-%D8%A5%D8%B9%D8%A7%D8%AF%D8%A9-%D8%A7%D9%84%D9%85%D8%AD%D8%A7%D9%88%D9%84%D8%A9)).
* يجيب خلال **10 ثوانٍ** إجمالًا (و**5 ثوانٍ** للاتصال). الأبطأ يُحسب timeout، ويُعاد كما يُعاد 5xx.
* يبدأ بـ `https://` ويقدّم TLS صالحًا. الشهادات تُتحقَّق بصرامة، فالشهادة الموقّعة ذاتيًا تفشل في كل محاولة.
* لا يُعيد التوجيه. لا نتبع `Location`؛ و`301`/`302` إخفاق.
* يقبل `POST` مع `Content-Type: application/json`.
* يقرأ **الجسم الخام** للتحقق من التوقيع (لا تُعد التسلسل).
* يكون **idempotent** — نفس `delivery_id` قد يصل أكثر من مرة.

فخ شائع في بعض الأطر: middleware يُعيد ترميز جسم JSON قبل أن يراه معالجك، فلا يطابق HMAC. الحلول:

* **Express:** استعمل `express.raw({ type: 'application/json' })` لمسار webhook ثم `JSON.parse(req.body)` في المعالج.
* **Django:** `request.body` هي البايتات الخام — هذا ما تريده.
* **Laravel:** `$request->getContent()` يُعيد الجسم الخام.
* **PHP خام:** `file_get_contents('php://input')`.

انظر [التحقق من التواقيع](https://dzbuild.com/ar/ar/api-docs/webhooks/verifying-signatures.md) لشيفرة كاملة بأربع لغات.
