Wocart

Webhooks

يرسل الـ webhook أحداث الأعمال (Business Events) الخاصة بمتجرك إلى عنوان على خادمك لحظة وقوعها: طلب جديد، طلب مؤكَّد، تسليم إلى شركة توصيل، توصيل ناجح. يُرسَل كل حدث بطلب POST بصيغة JSON، موقَّعًا وفق مواصفة Standard Webhooks، وتُعاد المحاولة إلى أن يقبله خادمك.

أما شركات التوصيل التي تبلّغ متجرك عن التسليمات فهي الاتجاه الآخر: اقرأ Webhooks شركات التوصيل.

إضافة webhook

في لوحة التحكم افتح التكاملات من القائمة الجانبية واختر إضافة webhook، وأعطه اسمًا وعنوان URL لخادمك والأحداث التي يستقبلها. وعبر الـ API، POST /api/event-consumers بصلاحية integrations:manage (وتعرض integrations:read الـ webhooks وعمليات تسليمها):

TerminalCode
curl -X POST http://localhost:8787/api/event-consumers \ -H "X-API-Key: cod_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"kind":"webhook","name":"Order system","url":"https://erp.example.dz/hooks/wocart","subscription":["order.*","shipment.delivered"]}'

يحتوي الرد على الـ webhook ومفتاح التوقيع الخاص به، whsec_…. لا يُعرض إلا هذه المرة: احفظه ضمن أسرار خادمك. وكل رد بعده يعرضه مُقنَّعًا.

  • يجب أن يكون العنوان https، عامًا على الإنترنت (لا عنوان خاصًا أو محليًا أو محجوزًا)، بلا اسم مستخدم ولا كلمة مرور.
  • للمتجر 10 webhooks على الأكثر.
  • يستقبل الـ webhook الأحداث المسجَّلة بعد إضافته. ولإرسال أحداث أقدم إليه استعمل إعادة الإرسال (Replay، انظر أدناه).

يسمّي الاشتراك أنواع الأحداث بدقة (order.confirmed)، أو حسب الموضوع (order.*، shipment.*)، أو كلها (*). والموضوع أو * يشمل أيضًا الأنواع التي تُضاف إلى الكتالوج لاحقًا.

ما يستقبله خادمك

كل إرسال هو طلب POST واحد إلى عنوانك:

الترويسةالقيمة
content-typeapplication/json
user-agentCodFlow-Webhooks/1
webhook-idمعرّف الحدث. هو نفسه في كل محاولة وكل إعادة إرسال.
webhook-timestampوقت إرسال هذه المحاولة، بثواني Unix.
webhook-signaturev1,<signature>، واحد لكل مفتاح توقيع، تفصل بينها مسافات.

جسم الطلب هو حدث الأعمال نفسه، تمامًا كما تعيده API سجل الأحداث:

JSONCode
{ "id": "0f9e8d7c-6b5a-4f3e-8d2c-1b0a09080702", "sequence": 1042, "type": "order.confirmed", "version": 1, "subject": { "type": "order", "id": "7f3c2a9e-4b1d-4e8a-9c55-2d6f0b8e1a47" }, "orderId": "7f3c2a9e-4b1d-4e8a-9c55-2d6f0b8e1a47", "occurredAt": "2026-10-25T09:40:55.000Z", "recordedAt": "2026-10-25T09:40:55.480Z", "actor": { "kind": "staff", "id": "1d3f5a7c-9e1b-4d3f-a5c7-9e1b3d5f7a9c", "name": "Samira" }, "origin": "dashboard", "data": { "order": { "id": "7f3c2a9e-4b1d-4e8a-9c55-2d6f0b8e1a47", "number": "ORD-20261025-0042", "status": "confirmed", "type": "online", "placedAt": "2026-10-25T09:14:03.120Z", "locale": "ar", "customer": { "id": "5a0e7c13-8f2b-4d9e-a6c4-1b3d5f7e9a20", "name": "Amina Benali", "phone": "0551234567", "email": null }, "destination": { "wilaya": { "id": 16, "name": "Alger", "nameAr": "الجزائر" }, "commune": { "id": "16001", "name": "Alger Centre", "nameAr": "الجزائر الوسطى" }, "city": null, "address": "12 rue Didouche Mourad", "deliveryType": "home", "stationCode": null }, "lines": [ { "id": "b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e", "productId": "e8a1c3b5-7d9f-4b2a-a4c6-8e0f2a4c6e8a", "variantId": "f1b3d5c7-9e1a-4c3b-b5d7-9f1b3d5f7a9c", "name": "Robe kabyle", "variantLabel": "Rouge / M", "sku": "RK-R-M", "quantity": 2, "unitPrice": { "amount": "4500.00", "currency": "DZD" }, "discount": { "amount": "500.00", "currency": "DZD" }, "total": { "amount": "8500.00", "currency": "DZD" }, "gift": false, "offer": { "id": "a7c9e1b3-5d7f-4a9c-8e1b-3d5f7a9c1e3b", "name": "La deuxième à -500 DA" }, "returnedQuantity": 0 } ], "subtotal": { "amount": "8500.00", "currency": "DZD" }, "deliveryFee": { "amount": "500.00", "currency": "DZD" }, "total": { "amount": "9000.00", "currency": "DZD" }, "coupon": null, "freeDeliveryOffer": null, "notes": "Appeler avant de passer", "customFields": [ { "id": "c3e5a7b9-1d3f-4b5a-9c7e-1f3a5b7d9e1f", "label": "Taille habituelle", "type": "select", "value": "M" } ] }, "previousStatus": "new" } }
الحقلمعناه
idالمعرّف الفريد للحدث. به تتعرّف على حدث عالجته من قبل.
sequenceموضعه في سجل أحداث المتجر: الحدث الأحدث رقمه أكبر.
type، versionنوع الحدث ونسخة data الخاصة به.
subjectموضوع الحدث: طلب أو شحنة.
orderIdالطلب المعني.
occurredAtمتى وقع: قد تبلّغ شركة التوصيل عن تسليم بعد ساعات.
recordedAtمتى سجّله المتجر.
actor، originمن تسبّب فيه (الفريق، وكيل ذكاء اصطناعي، الزبون، شركة التوصيل، النظام) وعبر ماذا (لوحة التحكم، الـ API، واجهة المتجر، webhook شركة التوصيل…).
dataالطلب، والشحنة في أحداث التوصيل، كما كانا لحظة وقوعه.

المبالغ نصوص عشرية مع عملتها: { "amount": "9000.00", "currency": "DZD" }. ولا يتجاوز أي حدث 64 كيلوبايت.

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

تحقّق من كل طلب قبل أن تتصرف بناءً عليه. التوقيع هو HMAC-SHA256 بترميز base64 للنص <webhook-id>.<webhook-timestamp>.<الجسم الخام>، ومفتاحه البايتات التي يرمّزها مفتاح التوقيع: الجزء الذي بعد whsec_ بعد فكّ ترميز base64.

  1. اقرأ الجسم الخام كما وصل. تحليل الـ JSON ثم إعادة كتابته يغيّر البايتات فلا يطابق التوقيع.
  2. ارفض أي webhook-timestamp يبعد أكثر من خمس دقائق عن ساعتك، حتى لا يُعاد إرسال طلب مُعترَض لاحقًا.
  3. اقبل الطلب إذا طابق أحدُ توقيعات v1, المفصولة بمسافات، بمقارنة ثابتة الزمن.

Node.js

JavascriptCode
import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 5 * 60; /** Whether a request came from your store, signed with this Signing Secret. */ export function verifyWebhook(secret, headers, rawBody) { const id = headers["webhook-id"]; const timestamp = Number(headers["webhook-timestamp"]); const signatures = headers["webhook-signature"]; if (!id || !signatures || !Number.isInteger(timestamp)) return false; if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false; const key = Buffer.from(secret.slice("whsec_".length), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest(); return signatures.split(" ").some((entry) => { const [version, signature = ""] = entry.split(","); const given = Buffer.from(signature, "base64"); return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected); }); }

مرّر الجسم الخام نصًّا، مثلًا عبر express.text({ type: "application/json" }).

PHP

PHPCode
<?php const TOLERANCE_SECONDS = 5 * 60; /** Whether a request came from your store, signed with this Signing Secret. */ function verify_webhook(string $secret, array $headers, string $rawBody): bool { $id = $headers['webhook-id'] ?? ''; $timestamp = $headers['webhook-timestamp'] ?? ''; $signatures = $headers['webhook-signature'] ?? ''; if ($id === '' || $signatures === '' || !ctype_digit($timestamp)) return false; if (abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) return false; $key = base64_decode(substr($secret, strlen('whsec_')), true); if ($key === false) return false; $expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$rawBody", $key, true)); foreach (explode(' ', $signatures) as $entry) { [$version, $signature] = array_pad(explode(',', $entry, 2), 2, ''); if ($version === 'v1' && hash_equals($expected, $signature)) return true; } return false; }

الجسم الخام هو file_get_contents('php://input') (وفي Laravel ‏$request->getContent())؛ ومرّر الترويسات بأسماء بأحرف صغيرة.

Python

Code
import base64 import hashlib import hmac import time TOLERANCE_SECONDS = 5 * 60 def verify_webhook(secret: str, headers: dict, raw_body: bytes) -> bool: """Whether a request came from your store, signed with this Signing Secret.""" msg_id = headers.get("webhook-id", "") timestamp = headers.get("webhook-timestamp", "") signatures = headers.get("webhook-signature", "") if not msg_id or not signatures or not timestamp.isdigit(): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{msg_id}.{timestamp}.".encode() + raw_body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() return any( version == "v1" and hmac.compare_digest(expected, signature) for version, _, signature in (entry.partition(",") for entry in signatures.split(" ")) )

الجسم الخام هو request.get_data() في Flask، أو await request.body() في FastAPI.

وتتحقق أي مكتبة Standard Webhooks من هذه الطلبات أيضًا، مثل new Webhook(secret).verify(rawBody, headers) مع حزمة standardwebhooks.

تغيير مفتاح التوقيع

تغيير مفتاح التوقيع في لوحة التحكم، أو POST /api/event-consumers/{id}/rotate-secret، يعطي مفتاحًا جديدًا مرة واحدة. ولمدة 24 ساعة يحمل كل طلب توقيعًا بكل مفتاح، فانقل خادمك إلى الجديد خلالها ولن يُرفض شيء. بعدها لا يوقّع إلا المفتاح الجديد.

أجب بسرعة

جوابك يحدّد ما يحدث بعد ذلك:

يجيب خادمكما يفعله المتجر
أي 2xx خلال 15 ثانيةتمّ التسليم.
410 Goneيوقف هذا الـ webhook ولا يرسل إليه شيئًا حتى تعيد تشغيله.
غير ذلك: حالة أخرى، أو إعادة توجيه، أو لا جواب في الوقت، أو تعذّر الاتصاليعيد المحاولة لاحقًا وفق الجدول أدناه.

أجب بـ 2xx بمجرد حفظ الحدث، ثم أنجز العمل. لا تُتَّبع إعادات التوجيه: أعطِ العنوان النهائي. وتُحترَم ترويسة Retry-After في الرفض، حتى يوم واحد.

إعادة المحاولة والتكرار والترتيب

يُحاوَل تسليم كل إرسال 8 مرات. تنطلق المحاولة الأولى بعد ثوانٍ من الحدث، وكل فشل ينتظر قبل التالية:

المحاولةالانتظار بعد السابقة
1لا انتظار: بعد ثوانٍ من الحدث
25 دقائق
330 دقيقة
4ساعتان
56 ساعات
612 ساعة
724 ساعة
824 ساعة

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

التسليم مرة واحدة على الأقل وبلا ترتيب مضمون:

  • قد تستقبل الحدث مرتين. احفظ webhook-id ما عالجته وتجاهله في المرة الثانية.
  • قد يصل حدثان بغير ترتيبهما. رتّبهما حسب sequence، أو حسب occurredAt لوقت وقوعهما.

الإرسال من جديد

من التكاملات ← الإرسالات في لوحة التحكم، أو عبر الـ API:

  • أعد المحاولة لإرسال واحد: POST /api/event-deliveries/{id}/retry.
  • أعد الإرسال… لمجال من الأحداث: POST /api/event-consumers/{id}/replay مع fromSequence وtoSequence، بحد أقصى 1000 حدث في كل مرة. وتُشمل الأحداث المسجَّلة أثناء توقف الـ webhook.

الحدث المُرسَل من جديد يحتفظ بمعرّفه، فيتعرّف عليه خادمك.

الأحداث التجريبية

إرسال حدث تجريبي في لوحة التحكم، أو POST /api/event-consumers/{id}/test مع type اختياري، يرسل فورًا إلى خادمك مثالًا لنوع حدث، موقَّعًا كغيره، ويعرض ما أجاب به خادمك. للحدث التجريبي sequence يساوي 0 وid يبدأ بـ test_؛ وهو ليس في سجل أحداثك.

أنواع الأحداث

نوع الحدثمتى
order.createdتم تقديم طلب: في المتجر، أو في لوحة التحكم، أو عبر الـ API، أو بواسطة وكيل ذكاء اصطناعي.
order.updatedعُدّل زبون الطلب أو وجهته أو ملاحظاته.
order.confirmedأكّد الزبون الطلب، عادةً عبر الهاتف.
order.unreachableتعذّر الوصول إلى الزبون لتأكيد الطلب.
order.preparingالطلب قيد التحضير.
order.readyالطلب مغلَّف وجاهز للتسليم إلى التوصيل.
order.cancelledأُلغي الطلب.
order.line_returnedرُفضت وحدات من سطر في الطلب عند الباب وعادت.
order.deletedحُذف الطلب؛ ويحمله الحدث كما كان.
shipment.handed_offسُلّم الطلب إلى شركة توصيل أو إلى أحد سائقي المتجر.
shipment.out_for_deliveryخرج الطرد نحو الزبون.
shipment.delivery_failedفشلت محاولة لتوصيل الطرد.
shipment.deliveredسُلّم الطرد ودُفع ثمنه عند الاستلام.
shipment.return_startedالطرد في طريق العودة إلى المتجر.
shipment.returnedعاد الطرد إلى المتجر.
shipment.cancelledأُلغيت الشحنة قبل توصيلها.
shipment.voidedأُلغي التسليم: الطلب لم يعد لدى التوصيل.

المخطط الكامل لكل نوع في مرجع الـ API تحت Webhooks.

انظر أيضًا