
إشعار Webhook هو الطريقة التي يعرف بها متجرك أو لوحة الفوترة أو البوت لديك أن العميل قد دفع. يرسل SnapEX Pay طلب HTTP POST إلى عنوان URL الخاص بك عند كل حدث يخص فاتورة أو دفعة أو عملية سحب. إذا أعددته بشكل صحيح، فهو أسرع إشارة لديك وأكثرها موثوقية. وإذا أعددته بإهمال، صار ثغرةً تتيح تعليم طلبات لم تُدفع قط على أنها مدفوعة. يتناول هذا الدليل الأجزاء المهمة.
الإعداد
افتح في لوحة تحكم التاجر صفحة «للمطورين» ← بطاقة «Webhook»، وأدخل عنوان URL الخاص بك واضغط «حفظ». لكل حساب عنوان URL واحد يستقبل جميع أنواع الأحداث. عند الحفظ الأول تعرض لوحة التحكم سر التوقيع whsec_… مرة واحدة فقط: احتفظ به على خادمك وحده.
يجب أن يكون عنوان URL عنوانًا مطلقًا يبدأ بـ https:// ومضيفه عام: بلا login:password فيه، وبلا localhost، ويجب ألا يشير اسم المضيف إلى عنوان IP خاص أو محجوز. لا تُتبَع عمليات إعادة التوجيه، لذا أعطِ عنوان URL النهائي.
ما الذي يصل
يحمل كل طلب أربع ترويسات:
| الترويسة | المعنى |
|---|---|
Webhook-Id |
معرّف فريد للحدث. لا يتغير عند إعادة المحاولة أو إعادة الإرسال اليدوية. |
Webhook-Timestamp |
وقت Unix (بالثواني) الذي أُرسلت فيه هذه المحاولة. لكل إعادة محاولة قيمة جديدة. |
Webhook-Signature |
البادئة v1= تليها قيمة HMAC-SHA256 بصيغة hex بأحرف صغيرة للسلسلة {Webhook-Timestamp}.{raw body}. |
Content-Type |
دائمًا application/json. |
يحتوي جسم الطلب على id (مطابق لـ Webhook-Id) وtype وcreated_at وdata. في أحداث invoice.* يكون data هو الفاتورة بالشكل نفسه الذي يعيده GET /invoices/{id}، بما في ذلك environment وamount_usd وstatus وexternal_id الخاص بك.
أحداث الفاتورة:
| الحدث | متى |
|---|---|
invoice.created |
أُنشئت الفاتورة، ولم تُختر طريقة دفع بعد. |
invoice.awaiting |
اختيرت طريقة دفع وصدر العنوان. |
invoice.underpaid |
وصل جزء من المبلغ؛ والعنوان نفسه ينتظر الباقي. |
invoice.paid |
دُفع المبلغ كاملًا وأُضيف إلى الرصيد. الإشارة الوحيدة لتسليم الطلب. |
invoice.partially_paid |
دُفع جزء فقط، وانتهت مهلة الاستكمال، وأُضيف الجزء المستلم إلى رصيدك. |
invoice.expired |
انتهت مهلة الفاتورة دون دفع. |
invoice.extra_payment |
وصل تحويل آخر بعد دفع الفاتورة؛ ويُضاف إلى الرصيد بشكل منفصل. |
تحقّق من التوقيع
- اقرأ جسم الطلب الخام كبايتات، قبل أي تحليل لـ JSON. فـ JSON المُعاد تسلسله لن يطابق التوقيع.
- احسب HMAC-SHA256 على
{Webhook-Timestamp}.{raw body}مستخدمًا السر كاملًا مفتاحًا، بما في ذلك البادئةwhsec_، ثم رمّز الناتج بصيغة hex. - قارن
v1=+ hex بالترويسةWebhook-Signatureباستخدام دالة مقارنة ثابتة الزمن. - ارفض أي
Webhook-Timestampيختلف عن ساعتك بأكثر من 5 دقائق. تحمل كل إعادة محاولة طابعًا زمنيًا جديدًا، لذا لا يُفسدها هذا الشرط.
بلغة PHP:
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… from the cabinet
$body = file_get_contents('php://input'); // raw body, before json_decode
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// skip if $event['id'] was already processed; do the heavy work in a queue
http_response_code(200);
بلغة Node.js (Express):
const crypto = require('crypto');
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('Webhook-Timestamp') || '';
const sig = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto
.createHmac('sha256', process.env.SNAPEX_WEBHOOK_SECRET)
.update(ts + '.' + req.body)
.digest('hex'));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// skip if event.id was already processed; queue the heavy work
res.sendStatus(200);
});
إعادة المحاولة والتكرار والترتيب
- النجاح هو أي استجابة 2xx. أي رمز آخر أو انتهاء مهلة أو خطأ في الشبكة يُعدّ فشلًا. المهلة 5 ثوانٍ للاتصال و15 ثانية للطلب كاملًا، لذا ردّ بسرعة ونفّذ العمل الثقيل بشكل غير متزامن.
- إعادة المحاولة. حتى 5 محاولات لكل حدث: المحاولة الأولى، ثم بعد كل فشل: بعد نحو 10 ثوانٍ، ثم بعد دقيقة، ثم بعد 5 دقائق، ثم بعد 15 دقيقة. بعد الفشل الخامس يتوقف التسليم التلقائي؛ وتعرض لوحة التحكم عمليات التسليم الأخيرة، وفيها زر «إعادة الإرسال» الذي يعيد إرسال الحدث بقيمة
Webhook-Idنفسها. - التكرار. التسليم مضمون «مرة واحدة على الأقل» (at-least-once): قد يصل الحدث نفسه أكثر من مرة. احفظ قيم
Webhook-Idالتي عالجتها وتجاهل ما يتكرر منها. - الترتيب. قد تصل الأحداث بغير ترتيبها: فقد يصل حدث قديم أُعيدت محاولة إرساله بعد حدث أحدث منه. لا تعتمد على الترتيب؛ قارن الحالة الواردة في الحدث بما لديك بالفعل.
- تحقّق مرة أخرى إن شئت. بعد وصول إشعار Webhook يمكنك إعادة قراءة الكائن عبر API:
GET /invoices/{id}.
اختبره بدون أموال حقيقية
أنشئ فاتورة تجريبية ("environment": "test")، وادفعها على صفحة الدفع، وراقب وصول إشعارات Webhook مع data.environment = "test". التفاصيل: كيف تختبر مدفوعات العملات المشفرة بدون أموال حقيقية.
الوحدات الجاهزة لـ WHMCS وBILLmanager وWooCommerce ومنصات أخرى تتحقق من التوقيع بالفعل، ولا تحتسب الدفعة مرتين أبدًا؛ راجع pay.snapex.pro. المرجع الكامل: توثيق API لـ SnapEX Pay.