منتجك يستقبل الدفع عن طريق Stripe، والدفع ينجح في Stripe، لكن الطلب في منتجك ما يتحدّث. في سجل السيرفر: No signatures found matching the expected signature for payload، وغالبًا معها سؤال من Stripe نفسه: Are you passing the raw request body you received from Stripe?. معناه إن سيرفرك رفض إشعار Stripe لأنه ما قدر يتأكد إنه فعلًا من Stripe.
إيش يصير هنا
لما ينجح الدفع، Stripe يرسل لسيرفرك إشعار (webhook) فيه تفاصيل العملية، ومعه توقيع في هيدر اسمه Stripe-Signature. سيرفرك يحسب التوقيع بنفسه من نص الإشعار ومن سر خاص (signing secret يبدأ بـ whsec_)، ويقارن. إذا تطابقوا، الإشعار حقيقي. وإذا لا، يرفضه. هذا اللي يمنع أي أحد يرسل لسيرفرك «العميل دفع» وهو ما دفع.
الأسباب الأشهر
- النص تغيّر قبل الفحص. التوقيع محسوب على النص الخام زي ما وصل، حرف بحرف. كثير من أطر العمل تحوّل النص لـ JSON قبل ما يوصل لكودك، وأول ما يتحوّل ويرجع نص، تتغيّر المسافات والترتيب والتوقيع ما يطابق. هذا السبب الأشهر، وهو اللي تسأل عنه رسالة Stripe.
- السر غلط. كل نقطة استقبال (endpoint) في Stripe لها سر خاص فيها، ووضع التجربة له أسرار غير الوضع الحقيقي. والسر اللي تعطيك إياه أداة
stripe listenعلى جهازك غير سر الـ endpoint في لوحة Stripe. وأحيانًا الكود يستخدم المفتاح السريsk_بدل سر الـ webhookwhsec_. - السر ناقص في النشر. متغير البيئة موجود على جهازك وناقص في إعدادات الاستضافة، فالكود يفحص بقيمة فاضية.
- Supabase Edge Functions: البيئة هناك Deno مو Node، والفحص لازم يكون بالدالة غير المتزامنة
constructEventAsync. الكود اللي ينسخه الذكاء الاصطناعي من أمثلة Node كثير ما يشتغل هناك.
خطأ قريب: Timestamp outside the tolerance zone
إذا الرسالة اللي عندك Timestamp outside the tolerance zone، التوقيع نفسه سليم، لكن الإشعار قديم بالنسبة لساعة سيرفرك. Stripe يرفض افتراضيًا أي إشعار عمره أكثر من خمس دقائق، عشان أحد ما يعيد إرسال إشعار قديم. السبب يكون إما ساعة السيرفر غلط، أو إن الإشعار تأخر في طابور عندك قبل ما يوصل للفحص. الحل إن ساعة السيرفر تتزامن تلقائيًا، والفحص يصير أول ما يوصل الطلب.
كيف تتأكد بنفسك
- افتح صفحة Webhooks في لوحة Stripe، واختار الـ endpoint. تشوف كل إشعار انرسل والرد اللي رجّعه سيرفرك. إذا كلها 400 أو 500، المشكلة عندك مو عند Stripe.
- تأكد إنك في الوضع الصح: Test أو Live. الـ endpoint وسرّه في الوضع الحقيقي غير اللي في التجربة.
- قارن آخر أحرف سر الـ endpoint في اللوحة بالقيمة اللي في إعدادات الاستضافة.
- بعد ما تصلّح، أعد إرسال إشعار فشل من نفس الصفحة، وشوف إذا صار الرد 200.
الحل اللي لا تسويه أبدًا
أخطر اقتراح نشوفه: «احذف سطر فحص التوقيع واقرأ JSON مباشرة». الدفع يرجع يشتغل، وفي نفس اللحظة صار أي أحد يعرف رابط الـ webhook يقدر يرسل له «تم الدفع» ويفعّل طلبات واشتراكات بدون ما يدفع ريال. والرابط مو سر: غالبًا شكله متوقع، مثل /api/webhooks/stripe.
الحل الصح
- مسار الـ webhook يستقبل النص الخام قبل أي تحويل لـ JSON، ويفحص التوقيع عليه. كل إطار عمل له طريقته، مثل
express.rawفي Express، أو قراءةreq.text()في Next.js. - سر الـ endpoint الصحيح، للوضع الصحيح، في إعدادات السيرفر فقط.
- بعد الفحص، كل عملية تتسجّل مرة وحدة برقمها، لأن Stripe ممكن يرسل نفس الإشعار أكثر من مرة. التفاصيل في قبل لا تفعّل الدفع في منتجك.
- السيرفر يرد على Stripe بسرعة، وأي شغل ثقيل يصير بعد الرد. إذا تأخر الرد، Stripe يعتبره فشل ويعيد المحاولة.
الدفع يحتاج أحد يتأكد منه
خطأ في الدفع يكلّف فلوس حقيقية، لك أو لعميلك. إذا المشكلة وحدة وواضحة، مكالمة استشارية تكفي. وإذا تبي أحد يصلّح مسار الدفع كامل ويختبره، الإصلاح المباشر. وإذا منتجك على Bolt، شوف مشاكل Bolt الأشهر، والدفع اللي يشتغل في التجربة فقط من أشهرها.