דילוג לתוכן הראשי
חזרה למדריכים
Implementation8 דק׳ קריאה

סוכן וואטסאפ בייצור: מ-Webhook עד Session Persistency

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

מה בונים כאן

ארבעה חלקים: Webhook שמקבל הודעות ממטא, חנות סשנים ששומרת context לכל משתמש, עיבוד הודעות בתור (queue) ולא ב-inline, ודה-דופליקציה שמונעת תשובות כפולות. אם מדלגים על אחד מהם — הסוכן יעבוד בדמו וישבור ביום השלישי בפרודקשן.

שלב 1: הגדרת ה-Webhook

ב-Meta App שלכם (WhatsApp > Configuration):

  1. ה-URL של ה-endpoint חייב להיות HTTPS עם certificate תקין.
  2. מטא שולחת בקשת אימות מסוג GET עם hub.mode=subscribe, hub.verify_token — עונים בחזרה עם הערך של hub.challenge. שורה אחת ב-Next.js/Express.
  3. כל בקשת POST מגיעה עם header של X-Hub-Signature-256 — HMAC-SHA256 על גוף הבקשה עם ה-App Secret. מאמתים לפני שנוגעים ב-content. בלי זה כל אחד יכול לשלוח לכם הודעות מזויפות.
  4. חוזרים עם 200 OK מהר — תוך שניות. לא מעבדים את ההודעה בתוך הבקשה. שומרים לתור ומחזירים 200. מטא שולחת מחדש כל webhook שלא קיבל 200, והיא תמשיך לנסות שעות — עיבוד איטי בתוך הבקשה יגרום לכם לקבל את אותה הודעה פעמיים, שלוש, עשר.

שלב 2: דה-דופליקציה — כי כפולים זה המצב הרגיל

ה-webhooks של WhatsApp מגיעים במדיניות at-least-once. כלומר: כפולים זה לא edge case, זה תנאי עבודה. ה-handler שלכם חייב להיות idempotent — עיבוד של אותו אירוע פעמיים מייצר אותה תוצאה כמו עיבוד פעם אחת.

המפתח: messages[].id (ה-wamid) להודעות נכנסות, ושילוב של מזהה ההודעה + סוג הסטטוס (sent/delivered/read) לעדכוני סטטוס. Redis עם SET-NX ו-TTL של כמה ימים עושה את העבודה:

const isNew = await redis.set(`msg:${payload.messages[0].id}`, 1, {
  nx: true,
  ex: 60 * 60 * 24 * 3,
});
if (!isNew) return; // כבר עיבדנו — מחזירים 200 בשקט

טעות נפוצה נוספת: ההודעות שאתם שולחים חוזרות אליכם כאירועי webhook. בלי סינון של הודעות יוצאות, הבוט שלכם ידבר עם עצמו עד שיגיע ל-rate limit.

שלב 3: חנות סשנים

כל שיחה מזוהה לפי מספר הטלפון של המשתמש (או contact identifier באינטגרציות חדשות). המבנה המינימלי שעובד:

  • Session key — per user, לא גלובלי. שני משתמשים בווצאפ זה שני סשנים, תמיד.
  • Context — היסטוריית השיחה (או summary שלה), מצב המשימה הנוכחית, last-seen.
  • TTL — סשנים נמחקים אחרי תקופה של חוסר פעילות. חודש אחד של שיחות לא צריך לשבת בזיכרון חי.

Redis לסשנים חמים + Postgres להיסטוריה לטווח ארוך זה השילוב הסטנדרטי. חשוב: אתם לא שולטים בתזמון. משתמש שולח הודעה, נעלם ל-20 דקות, וחוזר. הסוכן חייב לשרוד את הפער הזה בלי לאבד context — ולכן הסשן חי מחוץ לתהליך ה-request.

שלב 4: עיבוד אסינכרוני

ההודעה נכנסת → webhook מאשר קבלה (200) → ההודעה נכנסת לתור (SQS, BullMQ, Cloud Tasks — מה שכבר יש לכם) → worker מריץ את הסוכן → שולח תשובה. ככה אתם מקבלים retry-ים חינם, בידוד בין משתמשים, ויכולת לעבד עומסים.

מקצבים שצריך להכיר:

  • מספר הטלפון מתחיל מ-80 הודעות לשנייה (משודרג עד 1,000) — חריגה מחזירה שגיאת 130429.
  • יש מגבלת קצב per-user: בערך הודעה אחת כל כמה שניות לאותו נמען. בוט שמקליד ווליום של הודעות לאותו משתמש ייחתך.

שלב 5: חלון ה-24 שעות

הכלל שמעצב את כל הלוגיקה: כשמשתמש שולח הודעה, נפתח חלון של 24 שעות שבתוכו אתם יכולים לשלוח כל דבר. הודעה חדשה של המשתמש מאפסת את השעון — התשובות שלכם לא. אחרי 24 שעות, שליחת הודעה חופשית מחזירה שגיאת 131047, והדרך היחידה ליצור קשר היא template מאושר מראש (UTILITY / MARKETING / AUTHENTICATION — כל אחת עוברת אישור של מטא ומתומחרת לפי הודעה).

המשמעות המעשית: ה-state שלכם צריך לדעת בכל רגע אם החלון פתוח. שימרו lastInboundMessageAt בסשן וחשבו את serviceWindowExpiresAt לפני כל שליחה. אל תסמכו על try-catch סביב 131047 — זה נראה מקרי אבל בעצם המצב המרכזי של כל סוכן ווצאפ אמיתי.

טיפים מהשטח

  • זמן תגובה מטרה: מתחת ל-3 שניות. מעל זה, שלחו typing indicator ו-mark-as-read — המשתמש לא צריך להרגיש שנפל לבור.
  • ולידציה של חתימה, דה-דופליקציה, וספירת חלון — שלושת אלה שווים יותר מכל פרומפט מתוחכם.
  • לוג לכל tool call שהסוכן מבצע: קלט, פלט, latency, עלות. בלעדיו אי אפשר לדעת למה לקוח קיבל תשובה שגויה ב-3 לפנות בוקר.

רוצה להעמיק את הנושא אצלך בצוות?

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

בואו נדבר