חזרה לכל המאמרים
פתרון תקלות
9 דקות קריאה
18 ביולי 2026

Stripe Webhooks לא עובדים? הסיבות והתיקון המלא

מדריך פתרון תקלות ל-Stripe Webhooks: למה החתימה נכשלת, למה אירועים לא מעובדים, וכל התיקונים המדויקים שצריך.

תובנה מרכזית

תמצית המאמר (TL;DR)

מדריך פתרון תקלות ל-Stripe Webhooks: למה החתימה נכשלת, למה אירועים לא מעובדים, וכל התיקונים המדויקים שצריך.

Optimized for AI Extraction
Source: VibeScale Engineering Hub

TL;DR: אם Stripe Webhooks לא מפעילים כלום, החתימה נכשלת (signature verification failed), או האירועים לא מעובדים - הסיבה כמעט תמיד אחת מחמש: סוד webhook שגוי, גוף בקשה (raw body) שה-framework כבר פירסר ל-JSON לפני האימות, endpoint שלא נגיש מבחוץ, טיפול בסוגי אירועים לא נכונים, או היעדר idempotency. בנוסף: לעולם אל תסמכו על אישור מצד הלקוח כדי לתת גישה בתשלום. למטה יש אבחון מסודר וכל התיקון.

התסמין: מה בדיוק אתם רואים

הבעיה מתחלקת לשלושה תסמינים שנראים דומה אבל מקורם שונה, וחשוב להבחין ביניהם לפני שמתקנים.

תסמין א - שום דבר לא קורה. לקוח משלם ב-Stripe, התשלום מצליח בדשבורד של Stripe, אבל אצלכם במערכת לא קרה כלום. המשתמש לא שודרג, המנוי לא נפתח, ההזמנה לא סומנה כשולמה. ב-Stripe Dashboard תחת Developers ← Webhooks תראו שהאירוע נשלח אבל קיבל שגיאה, או שאין endpoint רשום בכלל.

תסמין ב - החתימה נכשלת. בלוגים של השרת מופיע Webhook signature verification failed או No signatures found matching the expected signature for payload. Stripe שולח את הבקשה, השרת מקבל אותה, אבל stripe.webhooks.constructEvent() זורק שגיאה ומחזיר 400.

תסמין ג - האירוע מתקבל אך לא מעובד. ה-handler רץ, מחזיר 200, אבל הלוגיקה העסקית לא מתבצעת - כי אתם מאזינים לסוג אירוע לא נכון, או שאותו אירוע מעובד פעמיים ויוצר כפילויות (חיוב כפול, מייל כפול, שתי הזמנות).

זיהוי נכון של התסמין חוסך שעות. תסמין ב הוא כמעט תמיד בעיית raw body או סוד שגוי. תסמין א הוא נגישות או endpoint לא רשום. תסמין ג הוא לוגיקה.

למה זה קורה: הסיבות מהנפוצה לנדירה

הבעיה נובעת כמעט תמיד מאחד מחמישה כשלים, מסודרים כאן לפי שכיחות במערכות ריאליות.

1. הגוף כבר פורסר ל-JSON לפני האימות (הסיבה מספר אחת). Stripe מחשב את החתימה על ה-raw bytes המדויקים של הבקשה. אם ה-framework שלכם (Express עם express.json(), Next.js API route, NestJS, Fastify) קרא את הגוף והמיר אותו לאובייקט JavaScript לפני שהגעתם לאימות, ה-bytes שאתם מעבירים ל-constructEvent כבר לא זהים למה ש-Stripe חתם עליו. גם JSON.stringify על האובייקט המפורסר לא יחזיר את אותם bytes בדיוק (סדר מפתחות, רווחים, escaping). התוצאה: החתימה תמיד נכשלת, גם כשהכל אחר תקין.

2. סוד ה-webhook שגוי או מעורבב. לכל endpoint ב-Stripe יש סוד חתימה נפרד (whsec_...). קל להתבלבל בין הסוד של הסביבה החיה לבין סביבת הטסט, בין הסוד של Stripe CLI המקומי לבין הסוד של ה-endpoint בפרודקשן, או פשוט לשכוח לעדכן את משתנה הסביבה בפרודקשן. סוד לא תואם מייצר בדיוק את אותה שגיאת חתימה.

3. ה-endpoint לא נגיש מבחוץ. Stripe הוא שרת חיצוני שצריך להגיע ל-URL הציבורי שלכם. אם ה-endpoint רץ על localhost בלי tunnel, חסום מאחורי אימות (auth middleware שדורש טוקן על נתיב ה-webhook), מוגן ב-Vercel Deployment Protection, או מחזיר redirect - Stripe לעולם לא יצליח למסור את האירוע.

4. אתם לא מאזינים לסוג האירוע הנכון. מפתחים רבים מאזינים ל-payment_intent.succeeded כשהם משתמשים ב-Checkout, אבל האירוע שמסמן השלמת רכישה ב-Checkout הוא checkout.session.completed. למנויים חוזרים צריך גם invoice.paid ו-customer.subscription.updated. אם ה-handler לא מכיר את הסוג שנשלח, הוא מחזיר 200 ולא עושה כלום.

5. אין idempotency - אין הגנה מפני כפילות. Stripe מבטיח מסירה "לפחות פעם אחת", לא "בדיוק פעם אחת". אותו אירוע יכול להגיע פעמיים (retry אחרי timeout, בעיית רשת). בלי בדיקת event.id מול טבלה בבסיס הנתונים, תעבדו את אותו תשלום פעמיים.

לעומק על התלות בין נגישות, הרשאות ואבטחת המידע ראו את מדריך RLS ואבטחה.

איך מתקנים: צעד אחר צעד

התיקון הוא רצף אבחון מסודר. עברו עליו לפי הסדר, כי כל שלב שולל סיבה.

שלב 1 - ודאו ש-Stripe בכלל מגיע אליכם. ב-Stripe Dashboard ← Developers ← Webhooks בחרו את ה-endpoint וראו את היסטוריית המסירות. אם אתם רואים ניסיונות עם קוד תגובה 4xx/5xx או timeout - Stripe מגיע, הבעיה אצלכם. אם אין endpoint רשום כלל, זו הבעיה: הוסיפו אותו עם ה-URL הציבורי המדויק.

לפיתוח מקומי השתמשו ב-Stripe CLI במקום לנחש:

stripe listen --forward-to localhost:3000/api/webhooks/stripe
# הפלט נותן סוד זמני: whsec_... - השתמשו בו מקומית
stripe trigger checkout.session.completed

שלב 2 - שמרו את ה-raw body (התיקון החשוב ביותר). נטרלו את ה-body parser על נתיב ה-webhook בלבד והעבירו ל-constructEvent את ה-buffer הגולמי.

ב-Express:

// חשוב: express.raw רק על נתיב ה-webhook, לפני express.json הכללי
app.post('/api/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,                          // Buffer גולמי, לא אובייקט
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      console.error('Webhook signature failed:', err.message);
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }
    // ... טיפול באירוע
    res.json({ received: true });
  }
);

ב-Next.js App Router (route handler) קראו את הגוף כטקסט גולמי:

export async function POST(req) {
  const body = await req.text();          // raw string, לא req.json()
  const sig = req.headers.get('stripe-signature');
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET
  );
  // ...
}

שלב 3 - התאימו את הסוד לסביבה. ודאו ש-STRIPE_WEBHOOK_SECRET בפרודקשן הוא הסוד של ה-endpoint הספציפי מ-Stripe Dashboard (ולא הסוד המקומי של ה-CLI ולא סוד סביבת הטסט). כל endpoint = סוד משלו. לאחר שינוי משתנה סביבה בפלטפורמת ה-hosting - צריך redeploy כדי שייכנס לתוקף.

שלב 4 - פתחו את הנתיב מפני חסימות. החריגו את נתיב ה-webhook מכל auth middleware, מ-CSRF ומכל בדיקת טוקן. ב-Vercel כבו Deployment Protection על הנתיב או הגדירו bypass. ודאו שהנתיב מחזיר ישירות 200/400 ולא redirect.

שלב 5 - האזינו לסוגי האירועים הנכונים ופצלו לוגיקה.

switch (event.type) {
  case 'checkout.session.completed':
    // הענקת גישה / סימון הזמנה כשולמה
    break;
  case 'invoice.paid':
    // חידוש מנוי
    break;
  case 'customer.subscription.deleted':
    // ביטול גישה
    break;
  default:
    console.log(`Unhandled event type: ${event.type}`);
}

שלב 6 - הוסיפו idempotency. לפני עיבוד, בדקו אם event.id כבר טופל. שמרו את המזהה בטבלה עם unique constraint; אם הוא כבר קיים - החזירו 200 ודלגו. כך retry לא יוצר כפילות. חשוב גם להחזיר 200 מהר: בצעו עבודה כבדה (מיילים, חיובים) בתור/רקע ולא בתוך ה-handler, כדי לא לגרום ל-timeout שמפעיל retry מיותר.

הערת אבטחה קריטית: לעולם אל תסמכו על הלקוח

הכלל שמפריד בין אינטגרציה בטוחה לפרצה: מקור האמת לתשלום הוא ה-webhook מהשרת של Stripe, לא הדפדפן.

אחרי תשלום, Stripe מפנה את המשתמש ל-success_url. מפתחים רבים מעניקים גישה או משדרגים חשבון על סמך ההגעה לדף ההצלחה או על סמך קריאה מצד הלקוח. זו טעות: כתובת ה-URL הזו ניתנת לזיוף, משתמש יכול לגשת אליה ישירות בלי לשלם. הענקת ההרשאות חייבת לקרות רק בתוך ה-webhook handler, אחרי אימות חתימה מוצלח, על אירוע checkout.session.completed עם payment_status: 'paid'.

באותה נשימה: ודאו שההרשאות בבסיס הנתונים אכן אוכפות זאת. אם טבלת המנויים פתוחה לכתיבה מהלקוח, כל האימות בצד השרת חסר ערך - משתמש פשוט יכתוב לעצמו הרשאה. זה בדיוק סוג הפער ש-מדיניות RLS ב-Supabase נועדה לסגור. אם המערכת נבנתה מהר בכלי Vibe Coding, הפער הזה נפוץ במיוחד.

איך למנוע את זה בעתיד

מניעה שיטתית חוסכת את החזרה על אותה תקלה בכל אינטגרציה.

  • בדקו מקומית עם Stripe CLI לפני כל דיפלוי: stripe listen + stripe trigger מדמים אירועים אמיתיים בלי לחכות לתשלום ממשי.
  • הפרידו נתיב webhook מכל שאר ה-API ותעדו במפורש שהוא דורש raw body. הוסיפו הערה בקוד ליד ה-handler כדי שהמפתח הבא לא יוסיף body parser גלובלי.
  • נטרו כשלים דרך היסטוריית ה-Webhooks ב-Stripe והגדירו התראה על כשלים חוזרים. Stripe עושה retry ימים אחדים - אם תתקנו בזמן, לא תאבדו אירועים.
  • בנו idempotency מההתחלה, לא כתיקון אחרי כפילות ראשונה.
  • החזיקו את מקור האמת בשרת תמיד, וגבו זאת בהרשאות בסיס נתונים אמיתיות.

מתי זה עמוק יותר מתיקון מהיר

לפעמים תקלת webhooks היא רק הסימפטום הגלוי של בעיה מבנית עמוקה יותר.

אם תיקנתם את החתימה וה-raw body אבל עדיין יש חיובים כפולים, מנויים שלא מסתנכרנים, או משתמשים שקיבלו גישה בלי לשלם - הבעיה כבר לא ב-webhook הבודד אלא בארכיטקטורת התשלומים כולה: היעדר idempotency רוחבי, הרשאות בסיס נתונים פרוצות, או הסתמכות על צד הלקוח שפזורה ברחבי הקוד. זה מצב נפוץ במערכות שנבנו במהירות עם כלי AI, שבהן זרימת התשלום נכתבה בלי מודל אבטחה עקבי.

במצב כזה כדאי אבחון מסודר של כל שרשרת התשלום וההרשאות. אפשר להתחיל מ-מסגרת האודיט שלנו או מ-אודיט מוכנות לפרודקשן, ולראות איך נראה חילוץ מערכת שקרסה בפרודקשן ב-מקרה של סוכן AI שנשבר בפרודקשן.

אם אתם צריכים יד מנוסה שתסגור את הפער בבטחה, שירות ה-חילוץ שלנו ב-VibeScale בדיוק לשם כך. דברו איתנו ישירות ב-WhatsApp ונבין יחד מה עומד מאחורי התקלה.

הצוות של VibeScale - מומחה VibeScale
Expert Verified Content

הצוות של VibeScale

צוות הנדסה ל-Vibe Coding Rescue

צוות מהנדסי תוכנה ומומחי ארכיטקטורה עם ניסיון מצטבר של מעל עשור בליווי סטארטאפים. אנחנו מובילים את VibeScale במטרה להפוך את ה-Vibe Coding לסטנדרט הנדסי בטוח ויעיל.

Auth ID: VBS-2026-AUTH

Security

Verified Code

Expertise

Cloud Architect

בואו נדבר על הפרויקט שלכם

מאמרים קשורים