اختبار الويب هوك أثناء التطوير: نفق HTTPS وحدث الاختبار وسجل التسليمات وتدوير السر
اختبار الويب هوك محليًا رغم رفض localhost: نفق HTTPS، وحدث اختبار موقَّع، وسجل التسليمات، والمحاولات الجديدة، وتدوير سر التوقيع دون انقطاع.
لاختبار الويب هوك محليًا، اجعلوا خادم التطوير لديكم متاحًا خلف نفق HTTPS، وسجّلوا العنوان العمومي للنفق كنقطة استقبال بمفتاح الاختبار، ثم أرسلوا لأنفسكم حدث الاختبار: payment.succeeded اصطناعي، موقَّع كحدث حقيقي. ترفض API العنوان localhost والعناوين الخاصة وأي عنوان غير HTTPS منذ لحظة التسجيل: فالنفق هو الطريق العادي، لا حيلة للالتفاف. يغطي هذا الدليل تسجيل نقطة الاستقبال، وحلقة التطوير، وقراءة سجل التسليمات، وجدول المحاولات الجديدة، والتعليق، وتدوير السر دون انقطاع، والفصل بين السندبوكس والإنتاج.
تسجيل نقطة الاستقبال، من بوابة التاجر أو عبر API
ChariPay منصة دفع مغربية، مزوّد خدمات دفع (PSP) تشغّله Chari Money، مؤسسة أداء مرخَّصة من بنك المغرب. والويب هوك فيها طلبات موقَّعة ترسلها خوادمنا إلى خادمكم كلما وقع شيء: أداء ناجح، أو استرداد، أو استحقاق اشتراك. والعنوان الذي يستقبلها يسمى نقطة استقبال. تسجّلونه من بوابة التاجر أو عبر API؛ وعبر API، فالمفتاح المستعمَل هو الذي يحدّد هل تنتمي نقطة الاستقبال إلى السندبوكس أم إلى الإنتاج.
| الحقل | الدور | ما يجب تذكّره |
|---|---|---|
url | عنوان الاستقبال | HTTPS عمومي، على المنفذ 443؛ وlocalhost والعناوين الخاصة والعناوين غير HTTPS مرفوضة منذ التسجيل |
enabledEvents | الأحداث المستقبَلة | يُستحسن قائمة صريحة؛ وإن تُركت فارغة فهي تعني كل الأحداث الحالية والمقبلة |
description | تسمية | للتعرف على نقطة الاستقبال في بوابة التاجر |
customHeaders | ترويسات توجيه ثابتة اختيارية | تُضاف إلى كل تسليم؛ ولا تسمّوها أبدًا باسم ترويسة ترسلها ChariPay |
وعبر API، يكفي نداء واحد للتسجيل:
curl -X POST 'https://api-psp.charipay.ma/api/v1/partner/webhooks/endpoints' \
-H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://votre-boutique.ma/webhooks/charipay",
"description": "Paiements boutique",
"enabledEvents": ["payment.succeeded", "refund.succeeded", "refund.failed"]
}'يحتوي الرد على signingSecret (whsec_…)، الذي لا يُعرض إلا مرة واحدة — عند الإنشاء، ثم عند كل تدوير — ولا تُرجعه أي قراءة لاحقة. احفظوه فورًا في خزنة أسرار أو في متغير بيئة، بالحيطة نفسها التي تعاملون بها المفتاح: والقواعد في حماية مفاتيح API. وقراءة نقطة الاستقبال أو تعديلها — العنوان، وقائمة الأحداث، والترويسات، والتفعيل — أو حذفها تتم عبر GET وPATCH وDELETE على /api/v1/partner/webhooks/endpoints/{id}، وكلها موصوفة في وحدة «الويب هوك» في توثيق API.
ويمكن لجلسة دفع أو لرابط دفع أن يحدّد أيضًا عنوان استقبال خاصًا به — config.urls.notification للجلسة، وnotificationUrl للرابط — يتلقى الأحداث الموقَّعة نفسها. وهذا مفيد لتوجيه متجر بعينه؛ أما في حلقة التطوير فانطلقوا من نقطة استقبال مسجَّلة، لأنها هي التي يُرسل إليها حدث الاختبار.
لماذا يُرفض localhost، وكيف تختبرون الويب هوك محليًا
القاعدة تُطبَّق منذ التسجيل: يجب أن يكون العنوان HTTPS عموميًا على المنفذ 443، وlocalhost والعناوين الخاصة والعناوين غير HTTPS مرفوضة. فالويب هوك ينطلق من خوادمنا نحو خادمكم: عنوان localhost أو عنوان خاص لا يشير إلى أي شيء يمكن بلوغه من الإنترنت، وعنوان غير مشفَّر سيُمرّر بيانات الأداء دون تشفير. والحل أثناء التطوير نفق HTTPS: خدمة تمنح جهازكم عنوانًا عموميًا يبدأ بـhttps:// وتمرّر الطلبات إلى خادمكم المحلي. وأي نفق يوفّر عنوان HTTPS عموميًا على المنفذ 443 يفي بالغرض؛ ولا نوصي بأي نفق بعينه.
حلقة التطوير، بالترتيب:
- 1شغّلوا خادمكم المحلي بمسار استقبال — مثلًا
/webhooks/charipay— يحتفظ بالجسم الخام للطلب. - 2افتحوا نفق HTTPS نحو هذا الخادم، ودوّنوا العنوان العمومي الذي يمنحه لكم.
- 3سجّلوا هذا العنوان متبوعًا بمساركم كنقطة استقبال في السندبوكس — من بوابة التاجر، أو بالنداء أعلاه بمفتاح الاختبار — واحفظوا
signingSecretفي إعداداتكم المحلية. - 4أرسلوا حدث الاختبار وراقبوا وصول الطلب في سجلات خادمكم.
- 5اقرؤوا سجل التسليمات لتروا بماذا أجاب خادمكم.
- 6نظّفوا بعد العمل: إن تغيّر عنوان النفق في كل تشغيل، فحدّثوا نقطة الاستقبال الموجودة عبر
PATCHبدل إنشاء أخرى، واحذفوا نقاط الاستقبال التطويرية المهجورة.
في الخطوة 3، لا ينقصكم إلا مفتاح اختبار لتسجيل نقطة استقبال في السندبوكس: ابدؤوا في وضع الاختبار. ثلاثة حقول — الاسم، والبريد الإلكتروني المهني، والمؤسسة —، ثم رابط تفعيل يصلكم بالبريد لاختيار كلمة المرور، ثم بوابة التاجر حيث تنشئون بأنفسكم مفتاح الاختبار بعد إعادة مصادقة. ووضع الاختبار مجاني، بلا حد زمني، والتحقق من مؤسستكم لا يشترطه إلا الانتقال إلى الإنتاج.
إرسال حدث الاختبار والتحقق من الرد
لكل نقطة استقبال مسجَّلة حدث اختبار، يُطلَق من بوابة التاجر أو عبر POST /api/v1/partner/webhooks/endpoints/{id}/test. وهو يرسل payment.succeeded اصطناعيًا يحمل Test: true، موقَّعًا تمامًا كحدث حقيقي، وينطلق حتى لو لم يكن payment.succeeded ضمن قائمة enabledEvents لديكم. ويُرجع النداء معرّف التسليم، الذي تقرؤونه بعد ذلك عبر GET /api/v1/partner/webhooks/events/{deliveryId} لتروا رد خادمكم.
ثلاثة تحققات تصنع اختبارًا جيدًا:
- التوقيع مقبول. إن فشل تحققكم على حدث الاختبار، فسيفشل على الأحداث الحقيقية. وسببان تتحققون منهما أولًا: جسم أُعيدت صياغته بدل الجسم الخام، أو سرّ نقطة استقبال أخرى.
- الرد من فئة `2xx`، وسريع. أجيبوا بـ
2xxبعد تسجيل إزالة التكرار والتحديث التجاري؛ والعمل الثقيل يُنقل إلى مهمة في الخلفية. فأي رد غير2xx، أو غياب الرد، يُطلق محاولات جديدة. - لا أثر تجاري. حدث الاختبار لا يقابله أي أداء حقيقي: تتعرف عليه شفرتكم من
Test: trueولا تعلّم أي طلبية بأنها مدفوعة.
لا نعيد هنا شفرة المستقبِل — التحقق على الجسم الخام، وإزالة التكرار، وتأكيد الاستلام —: فهي مشروحة خطوة بخطوة في إنجاح دمج الويب هوك، مع الأخطاء الخمسة المعروفة. وبعد نجاح حدث الاختبار، انتقلوا إلى أداء حقيقي في السندبوكس مع اختبار الدفع في وضع الاختبار: بطاقة الاختبار، والعودة إلى صفحتكم، وحدث حقيقي.
قراءة سجل التسليمات
يجيب سجل التسليمات عن سؤالي كل تتبّع للأخطاء: هل أرسلنا، وبماذا أجاب خادمكم؟ ويُطَّلع عليه في بوابة التاجر أو عبر API. فـGET /api/v1/partner/webhooks/events يسرد الأحداث المرسلة لبيئة مفتاحكم، من الأحدث إلى الأقدم، مع مرشّحات حسب نقطة الاستقبال ونوع الحدث وحالة التسليم والفترة. وGET /api/v1/partner/webhooks/events/{deliveryId} يفصّل تسليمًا واحدًا: عدد المحاولات، ورمز HTTP الذي أعاده خادمكم في آخر محاولة، والمحاولة التالية المبرمجة، والجسم الدقيق الذي وُقّع وأُرسل — ما يكفي لإعادة حساب التوقيع محليًا حين يفشل تحققكم.
وست ترويسات للتسليم يجب معرفتها، بكتابتها وحالة أحرفها:
| الترويسة | ما تحمله | فائدتها لكم |
|---|---|---|
X-CHARI-SIGNATURE | توقيع التسليم | التحقق من الأصالة |
X-CHARI-SIGNATURE-NEXT | التوقيع بالسر الجديد، أثناء التدوير فقط | تبديل السر دون انقطاع |
X-CHARI-TIMESTAMP | لحظة الإرسال، بالمللي ثانية منذ epoch | الدخول في التحقق، ورفض إعادة الإرسال الخبيثة |
Chari-Webhook-Id | معرّف التسليم | ربط طلب مستلَم بتسليم |
Chari-Event-Id | معرّف الحدث، الثابت عبر كل المحاولات وكل نقاط الاستقبال لديكم | إزالة التكرار |
Chari-Event-Type | نوع الحدث، مثل payment.succeeded | توجيه المعالجة |
أزيلوا التكرار على أساس Chari-Event-Id، لا على أساس Chari-Webhook-Id — فهذا الأخير يحدّد التسليم، لا الحدث. وحين تحققون في طلبية، دوّنوا الاثنين: الأول يخبركم بأي حدث يتعلق الأمر، والثاني بأي تسليم تنظرون فيه. ولإيجاد الطلبية وراء حدث حقيقي، الطريقة في ربط الأداء بالطلبية.
المحاولات الجديدة والتعليق وإعادة التفعيل
أي رد غير 2xx — أو غياب الرد — يُطلق محاولات جديدة، وفق جدول ثابت:
| المحاولة | التوقيت |
|---|---|
| الأولى | فورية |
| الثانية | بعد دقيقة من السابقة |
| الثالثة | بعد 5 دقائق من السابقة |
| الرابعة | بعد 30 دقيقة من السابقة |
| الخامسة | بعد ساعة من السابقة |
| التالية | كل 6 ساعات، حتى 16 محاولة على مدى نحو 72 ساعة |
بعد ذلك، يُعلَّم التسليم بأنه فاشل؛ ويُعاد إرساله بنقرة واحدة من بوابة التاجر. ونقطة الاستقبال التي تتراكم إخفاقاتها تُعلَّق — والعتبة غير منشورة. أصلحوا السبب، وأعيدوا تفعيل نقطة الاستقبال من بوابة التاجر أو عبر POST /api/v1/partner/webhooks/endpoints/{id}/activate، ثم تحققوا بحدث الاختبار. وتُحفظ الأحداث المنتظرة أثناء التعليق، وتُستأنف بالترتيب عند إعادة التفعيل.
ولهذا الجدول أثر عملي أثناء التطوير: نفق مغلق في المساء يراكم الإخفاقات على نقطة الاستقبال الخاصة بالسندبوكس. عطّلوا نقطة الاستقبال عبر PATCH حين تتوقفون عن العمل: وإلا فقد تؤدي الإخفاقات المتكررة إلى تعليقها، وسيلزمكم إعادة تفعيلها. وفي الإنتاج، بعد انقطاع أطول من نافذة المحاولات، لا تنتظروا الويب هوك: طابقوا عبر GET /v1/transactions، الذي يسرد معاملات الفترة.
تدوير سر الويب هوك دون انقطاع
يُغيَّر السر حين يغادر أحد أعضاء الفريق، أو حين يُحتمل أنه تسرّب، أو ببساطة بشكل دوري. والتدوير مصمَّم كي لا يضيع أي حدث:
- 1اطلبوا سرًا جديدًا من بوابة التاجر أو عبر
POST /api/v1/partner/webhooks/endpoints/{id}/rotate-secret: يحتويه الرد، مرة واحدة فقط. - 2أثناء التدوير، يحمل كل تسليم توقيعين:
X-CHARI-SIGNATUREمحسوبًا بالسر القديم، وX-CHARI-SIGNATURE-NEXTمحسوبًا بالسر الجديد. - 3اقبلوا أيًّا منهما: تحققوا بالسر الذي تملكونه — القديم مقابل
X-CHARI-SIGNATURE، والجديد مقابلX-CHARI-SIGNATURE-NEXT. - 4انشروا السر الجديد على كل خوادمكم، ثم حوّلوا تحققكم إليه: مقابل
X-CHARI-SIGNATURE-NEXTما دامت هذه الترويسة حاضرة، ثم مقابلX-CHARI-SIGNATUREبعد ذلك. - 5احذفوا السر القديم من إعداداتكم بعد إتمام التحويل.
وما دام الحقل previousSecretExpiresAt الخاص بنقطة الاستقبال مملوءًا، يظل السر القديم صالحًا للتحقق ويُرسل التوقيعان معًا. وبعد انتهاء التدوير، تختفي X-CHARI-SIGNATURE-NEXT وتُحسب X-CHARI-SIGNATURE بالسر الجديد، الذي يصبح السر الوحيد لنقطة الاستقبال: فالتحقق الذي لا يقرأ إلا X-CHARI-SIGNATURE-NEXT سيرفض حينها كل تسليم. أما التحقق نفسه فلا يتغير: إنها الدالة ذاتها، تُنادى بزوج آخر من التوقيع والسر.
const crypto = require('crypto');
function verify(rawBody, signature, timestamp, secret) {
// ±5-minute anti-replay window — the timestamp is in milliseconds.
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// A malformed signature must return false — never throw (500 → retries).
if (!/^[0-9a-f]{64}$/i.test(signature)) return false;
// Constant-time comparison: never `===`.
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}تمرّنوا على التدوير في السندبوكس قبل أن تحتاجوه في الإنتاج: اطلبوا سرًا جديدًا، وأرسلوا حدث الاختبار، وتحققوا في السجل من أن خادمكم أجاب بـ2xx بالسر القديم، ثم بالسر الجديد.
السندبوكس والإنتاج: نقطتا استقبال وسرّان
تنتمي نقطة الاستقبال إلى بيئة واحدة، هي بيئة المفتاح الذي سجّلها حين تمرّون عبر API: نقطة السندبوكس لا تتلقى إلا أحداث السندبوكس، ونقطة الإنتاج لا تتلقى إلا أحداث الإنتاج، وسرّاهما مختلفان. ولا تنتقل نقطة الاستقبال من بيئة إلى أخرى: عند الإطلاق، تسجّلون نقطة جديدة بمفتاح الإنتاج. وفي الشفرة لا تغيّرون سوى المفتاح وسرّ الويب هوك: فالعناوين والحمولات ورموز الأخطاء متطابقة. ومن الأخطاء التي ينبغي تجنّبها الإبقاء على سر السندبوكس في الإنتاج: فيرفض تحققكم حينها كل إشعار.
اختاروا أحداثكم من بين العشرين التي توثّقها وحدة «فهرس الأحداث» في توثيق API — المدفوعات (payment.succeeded وpayment.failed وorder.paid)، والاشتراكات، والاستردادات، وحركات الحساب، والتوزيع — ووجّهوا المعالجة دائمًا على أساس الترويسة Chari-Event-Type. ولا يُرسل payment.failed إلا إذا طلبتموه عند إنشاء الجلسة (notifyOnFailure): فلا تبنوا عليه أي مسار. وبالنسبة إلى رابط يُسدَّد نقدًا في الوكالة، تابعوا حالة الرابط: بمجرد أن تؤكد الشبكة الإيداع، يصبح الرابط «مدفوعًا» ويصلكم الإشعار في بوابة التاجر؛ ومن جهة الخادم تُقرأ حالة الرابط عبر GET /v1/payment-links/{reference}.
وأخيرًا، قد يصل الحدث نفسه أكثر من مرة، وبغير ترتيب: وهذا مقصود في التصميم. والتعليل الكامل — لماذا تُزال التكرارات وأين — في Idempotency في المدفوعات.
أسئلة شائعة
هل يمكن تسجيل عنوان localhost كنقطة استقبال للويب هوك؟
لا. يجب أن يكون العنوان HTTPS عموميًا على المنفذ 443: فـlocalhost والعناوين الخاصة والعناوين غير HTTPS مرفوضة منذ التسجيل. أثناء التطوير، افتحوا نفق HTTPS نحو خادمكم المحلي وسجّلوا العنوان العمومي الذي يمنحه لكم، بمفتاح الاختبار. وإن تغيّر هذا العنوان، فحدّثوا نقطة الاستقبال عبر PATCH بدل إنشاء أخرى.
كيف أرسل حدث اختبار إلى نقطة الاستقبال؟
من بوابة التاجر، أو عبر POST /api/v1/partner/webhooks/endpoints/{id}/test. يرسل النداء payment.succeeded اصطناعيًا يحمل Test: true، موقَّعًا كحدث حقيقي، حتى لو لم يكن هذا الحدث في قائمتكم. ويُرجع معرّف التسليم: اقرؤوه عبر GET /api/v1/partner/webhooks/events/{deliveryId} لتروا بماذا أجاب خادمكم. وهكذا تتأكدون من سلامة التوقيع قبل أي أداء.
كيف أميّز حدث الاختبار عن أداء حقيقي؟
يحمل حدث الاختبار Test: true ولا يقابله أي أداء حقيقي. وتوقيعه مطابق لتوقيع الحدث الحقيقي، فلا تعتمدوا عليه للتمييز. يجب أن تتعرف شفرتكم على Test: true، وأن تجيب بـ2xx، وألا تُحدث أي أثر تجاري — لا طلبية تُعلَّم مدفوعة، ولا بريد يُرسل إلى الزبون.
كم مرة يُعاد إرسال الويب هوك حين لا يجيب خادمي؟
دون رد 2xx، تتبع المحاولةَ الأولى الفورية محاولاتٌ جديدة بعد دقيقة، ثم 5 دقائق، ثم 30 دقيقة، ثم ساعة، ثم كل 6 ساعات، حتى 16 محاولة على مدى نحو 72 ساعة. بعد ذلك يُعلَّم التسليم فاشلًا ويُعاد بنقرة واحدة من بوابة التاجر. وبعد انقطاع أطول، طابقوا عبر GET /v1/transactions.
ماذا أفعل حين تُعلَّق نقطة استقبال الويب هوك؟
تُعلَّق نقطة الاستقبال بعد إخفاقات متكررة. أصلحوا السبب أولًا بالاستعانة بسجل التسليمات — رمز HTTP، وزمن الرد، والتوقيع —، ثم أعيدوا تفعيلها من بوابة التاجر أو عبر POST /api/v1/partner/webhooks/endpoints/{id}/activate. تُحفظ الأحداث المنتظرة أثناء التعليق وتُستأنف بالترتيب عند إعادة التفعيل؛ ثم تحققوا من كل شيء بحدث الاختبار.
كيف أدوّر سر الويب هوك دون أن أفقد أحداثًا؟
اطلبوا سرًا جديدًا من بوابة التاجر أو عبر POST …/endpoints/{id}/rotate-secret. أثناء التدوير، يحمل كل تسليم X-CHARI-SIGNATURE بالسر القديم وX-CHARI-SIGNATURE-NEXT بالسر الجديد: اقبلوا أيًّا منهما. انشروا السر الجديد على كل خوادمكم، وحوّلوا تحققكم إليه، ثم احذفوا السر القديم من إعداداتكم: فبعد انتهاء التدوير يُتحقَّق من السر الجديد مقابل X-CHARI-SIGNATURE. وتمرّنوا على ذلك في السندبوكس قبل الإنتاج.
هل سر الويب هوك هو نفسه في السندبوكس وفي الإنتاج؟
لا. تنتمي كل نقطة استقبال إلى بيئة واحدة — عبر API، بيئة المفتاح الذي سجّلها — وتتلقى سرّها الخاص: فسرّ السندبوكس لا يتحقق أبدًا من تسليم في الإنتاج. وعند الانتقال إلى الإنتاج، سجّلوا نقطة استقبال جديدة بمفتاح الإنتاج واحفظوا سرّها منفصلًا: فالمفتاح وسرّ الويب هوك هما الشيئان الوحيدان اللذان يتغيران.
ماذا أفعل إن أضعت سر التوقيع؟
لا يُعرض السر إلا عند الإنشاء وعند التدوير: ولا تُرجعه أي قراءة. أطلقوا تدويرًا من بوابة التاجر أو عبر API للحصول على سر جديد، يُعرض مرة واحدة. وأثناء التدوير، تحققوا من X-CHARI-SIGNATURE-NEXT بهذا السر الجديد، ثم من X-CHARI-SIGNATURE بعد انتهاء التدوير، واحفظوه هذه المرة في خزنة أسرار، إلى جانب مفتاح API.
الخطوة التالية
سجّلوا نقطة استقبال في السندبوكس خلف نفق HTTPS، وأرسلوا لأنفسكم حدث الاختبار، واقرؤوا الرد في السجل: ابدؤوا في وضع الاختبار يمنحكم المفتاح اللازم. ثم اكتبوا المستقبِل مع إنجاح دمج الويب هوك، وأجروا أداءً حقيقيًا في السندبوكس مع اختبار الدفع في وضع الاختبار.
بقلم فريق ChariPay.