סוכן וואטסאפ בייצור: מ-Webhook עד Session Persistency
מדריך שלב-אחר-שלב להקמת סוכן וואטסאפ שעובד באמת: חיבור Webhook, ניהול סשנים מרובי משתמשים, שמירת מצב בין הודעות וטיפול בהודעות חוזרות.
מה בונים כאן
ארבעה חלקים: Webhook שמקבל הודעות ממטא, חנות סשנים ששומרת context לכל משתמש, עיבוד הודעות בתור (queue) ולא ב-inline, ודה-דופליקציה שמונעת תשובות כפולות. אם מדלגים על אחד מהם — הסוכן יעבוד בדמו וישבור ביום השלישי בפרודקשן.
שלב 1: הגדרת ה-Webhook
ב-Meta App שלכם (WhatsApp > Configuration):
- ה-URL של ה-endpoint חייב להיות HTTPS עם certificate תקין.
- מטא שולחת בקשת אימות מסוג GET עם
hub.mode=subscribe,hub.verify_token— עונים בחזרה עם הערך שלhub.challenge. שורה אחת ב-Next.js/Express. - כל בקשת POST מגיעה עם header של
X-Hub-Signature-256— HMAC-SHA256 על גוף הבקשה עם ה-App Secret. מאמתים לפני שנוגעים ב-content. בלי זה כל אחד יכול לשלוח לכם הודעות מזויפות. - חוזרים עם
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 לפנות בוקר.
רוצה להעמיק את הנושא אצלך בצוות?
סדנאות מעשיות והרצאות שמבוססות על הניסויים מהשטח — כולל קוד חי ותרגול.
בואו נדבר