أنشئ Checkout من الخادم بسعر معروف، وضع معرف العميل والمنتج في بيانات مخصصة. عند وصول Webhook: اقرأ الجسم الخام، تحقق من التوقيع، خزّن معرف الحدث بقيد فريد، أرجع 2xx بسرعة، ثم عالج الحدث في Queue. حدّث الوصول من حالة الدفع أو الاشتراك، لا من صفحة النجاح ولا من قيمة أرسلها المتصفح.
بطاقة سريعة
- Endpoint
- POST عام عبر HTTPS
- المدخل الصحيح
- Raw body + Signature header
- مفتاح التكرار
- معرف الحدث عند المزود
- تاريخ التحقق
- 17 أكتوبر 2026
افصل Checkout عن منح الوصول
المتصفح يطلب جلسة شراء من خادمك. الخادم يختار السعر من قاعدة بياناته ويرسل إلى المزود بيانات مرجعية غير حساسة مثل معرف العميل والمنتج. بعد الدفع، قد يعود العميل إلى صفحة نجاح، لكن هذه الصفحة تعرض «جارٍ تأكيد الدفع» فقط. منح الوصول ينتظر حدثاً موقّعاً من المزود.
- الخادم ينشئ جلسة بسعر ومعرف منتج معروفين
- العميل يدفع على Checkout المستضاف أو المدمج
- المزود يرسل Webhook إلى Endpoint عام
- Endpoint يتحقق من التوقيع ويحفظ الحدث الخام
- المعالج الخلفي يطابق العميل والمنتج والمبلغ والعملة
- معاملة قاعدة بيانات واحدة تحدّث الدفع والصلاحية
- صفحة النتيجة تستعلم من خادمك أو تستقبل نتيجة موقعة
المبدأ واحد، لكن التوقيع والأحداث تختلف
الصور التالية من صفحات Webhooks الرسمية. لا تنسخ كود مزود إلى آخر؛ استخدم SDK الرسمي أو خوارزمية التحقق المحددة لكل منصة.



تحقق من التوقيع قبل JSON.parse
توقيع Webhook يُحسب على البايتات الأصلية. إذا حوّل Framework الجسم إلى JSON ثم أعاد ترتيبه أو غيّر المسافات، قد يفشل التحقق. احصل على Raw body أولاً، اقرأ Header الصحيح، واستخدم السر الخاص بنفس Endpoint والبيئة.
| المزود | Header | المادة الموقعة | التوصية |
|---|---|---|---|
| Paddle | Paddle-Signature | الوقت + نقطتان + الجسم الخام | SDK رسمي وتحقق من حد الزمن |
| Stripe | Stripe-Signature | الجسم الخام وفق سر Endpoint | دالة SDK المخصصة |
| Lemon Squeezy | X-Signature | HMAC-SHA256 للجسم الخام | مقارنة ثابتة الزمن |
لا تسجل السر أو الجسم الكامل إذا كان يحتوي بيانات شخصية. افصل أسرار Sandbox عن Production، وبدّلها بطريقة تسمح بفترة انتقال إن دعم المزود ذلك.
افترض أن الحدث نفسه سيصل أكثر من مرة
Paddle توثق تسليماً «مرة واحدة على الأقل»، وStripe وLemon Squeezy يعيدان المحاولة عند الفشل. إذا كان كل وصول يضيف رصيداً أو يرسل ترخيصاً جديداً، سيتكرر التنفيذ. ضع قيداً فريداً على معرف الحدث، وسجّل نتيجة المعالجة داخل معاملة.
- INSERT للحدث مع Unique(provider, event_id)
- إذا تعارض المفتاح، أعد 2xx ولا تنفذ الأثر ثانية
- اجعل منح الرصيد نفسه قابلاً للتكرار بمعرف العملية
- لا تستخدم البريد الإلكتروني كمفتاح للحدث أو العميل
- احتفظ بعدد المحاولات والخطأ الأخير ووقت النجاح
الوصول المتأخر لا يجب أن يعيد الحالة إلى الوراء
قد يصل إلغاء قبل تحديث سابق، أو يصل نجاح الدفع بعد محاولة فاشلة. Paddle تطلب استخدام occurred_at لا وقت الاستلام لفهم الترتيب. في الأنظمة الأخرى، استخدم وقت إنشاء الحدث أو استرجع الكائن الحالي من API عندما تكون الحالة حساسة.
خزن آخر نسخة أو آخر وقت حدث مقبول لكل اشتراك. لا تجعل حدثاً أقدم يستبدل حالة أحدث. ومع ذلك، تعامل مع الاسترجاع والنزاع كأحداث مالية مستقلة تحتاج أثراً واضحاً، لا مجرد مقارنة زمنية عمياء.
احفظ أولاً ثم عالج خارج الطلب
الاستجابة السريعة تمنع إعادة المحاولات غير الضرورية، بينما Queue تمنحك إعادة تنفيذ مراقبة إذا تعطلت قاعدة البيانات أو خدمة البريد.

ارسم آلة حالات قبل كتابة switch كبير
عملية لمرة واحدة قد تنتقل من Pending إلى Paid ثم Refunded أو Disputed. الاشتراك يحتاج حالات Trialing وActive وPast due وPaused وCanceled وExpired. اربط كل حالة بقرار وصول واضح، وحدد فترة السماح وسياسة الاسترجاع.
| الحدث التجاري | ما تخزنه | قرار الوصول |
|---|---|---|
| دفع مكتمل | المعاملة والمبلغ والعملة والمنتج | منح الوصول مرة واحدة |
| تجديد ناجح | الدورة الجديدة وتاريخ الانتهاء | تمديد الصلاحية |
| فشل دفع | المحاولة والحالة وموعد الإعادة | فترة سماح وفق السياسة |
| إلغاء اشتراك | ends_at والتغيير المجدول | الوصول حتى نهاية المدفوع غالباً |
| استرجاع أو نزاع | المبلغ والسبب والمرجع | تطبيق السياسة وتسجيل التدخل |
افصل الحدث والدفع والاشتراك والصلاحية
لا تضع كل شيء في عمود واحد. جدول أحداث يحفظ الاستلام والمعالجة، جدول معاملات يحفظ المال، جدول اشتراكات يعكس حالة المزود، وجدول صلاحيات يحدد ما يستطيع العميل استعماله. هذا الفصل يجعل المصالحة والتدقيق وإصلاح الأخطاء ممكنة.
- provider، environment، event_id، event_type، occurred_at
- customer_id داخلي ومعرف العميل لدى المزود
- transaction_id، subscription_id، price_id، product_id
- amount، currency، status، paid_at، refunded_at
- processed_at، attempts، last_error، payload_hash
- entitlement_key، starts_at، ends_at، revoked_at
ابنِ Adapter موحداً، لا Endpoint عامّاً بلا حدود
يمكن أن تستقبل كل بوابة في مسار منفصل مثل /api/webhooks/paddle و/api/webhooks/stripe. كل Adapter يتحقق من توقيعه ويحوله إلى أحداث داخلية قليلة: PaymentPaid وSubscriptionChanged وRefundCreated. منطق الصلاحيات يستهلك الأحداث الداخلية ولا يعرف أسماء المزود.
Paddle توصي بالرد خلال خمس ثوانٍ وتوفر Simulator. Stripe توصي برد 2xx سريعاً قبل المنطق المعقد، وتوفر CLI للاختبار المحلي. Lemon Squeezy يعيد المحاولة حتى ثلاث مرات بعد المحاولة الأولى ويتيح محاكاة أحداث الاشتراك في Test mode. صمّم Queue والمراقبة وفق ضمانات المزود الفعلية.
اختبر الفشل المتعمد، لا المسار السعيد فقط
- توقيع صحيح وخاطئ وجسم تم تغييره
- الحدث نفسه مرتين وعشرين مرة بالتزامن
- حدثان خارج الترتيب
- قاعدة البيانات متوقفة بعد حفظ الحدث
- Queue تفشل ثم تعيد المحاولة
- دفع ناجح ثم استرجاع جزئي وكامل
- اشتراك جديد وتجديد وفشل وإلغاء وانتهاء
- Sandbox لا يستطيع تعديل جداول Production
- إعادة إرسال الحدث من لوحة المزود
بعد الاختبار، قارن رصيد المزود بسجل معاملاتك وصلاحيات العملاء. أي فرق يجب أن يظهر في تقرير مصالحة وتنبيه، لا أن ينتظر شكوى العميل.
لا تطلق قبل اكتمال هذه النقاط
- HTTPS وPOST فقط مع حد لحجم الطلب
- Raw body محفوظ مؤقتاً للتحقق من التوقيع
- أسرار منفصلة لكل مزود وبيئة وEndpoint
- Unique key للأحداث ومعالجة ذرّية للصلاحيات
- استجابة 2xx سريعة ثم معالجة غير متزامنة
- دعم الأحداث المتأخرة والمكررة
- سجل تدقيق ومصالحة وتنبيهات للفشل
- اختبارات Sandbox ومحاكاة وإعادة إرسال
- صفحة نتيجة لا تثق ببارامترات المتصفح
اختر البوابة قبل تصميم أحداثك
قارن نموذج Merchant of Record والفوترة والرسوم والأهلية، ثم طبّق Adapter يناسب أحداث المزود المختار.
