מפתחים

קבלת SMS בקוד: תשובות מהמספר הווירטואלי ישר ל-CRM

4 בנובמבר 2026·10 דק' קריאה·מאת ניצן סויסה

כדי לקבל SMS בקוד צריך מספר שמקבל הודעות ודרך להעביר אותן לשרת שלך. ב-Sendy המספר הווירטואלי מקבל את התשובות, וכל הודעה נכנסת מגיעה אליך באחת משתי דרכים: webhook ברגע שהיא נוחתת, או קריאת GET /v1/inbound שאתה מריץ מתי שנוח לך.

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

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

מה צריך מראש

  1. מספר וירטואלי פעיל. בעל מרחב העבודה מפעיל אותו מהתפריט "מספר וירטואלי". המספר מוקצה מתוך מאגר, אחד לכל מרחב עבודה. הצעדים: הפעלת מספר וירטואלי.
  2. טוקן API. יוצרים אותו בעמוד מפתחים, ושולחים אותו בכל קריאה ב-header Authorization: Bearer.
  3. לדרך של ה-webhook: כתובת HTTPS פומבית בשרת שלך, ובעלות על מרחב העבודה כדי להגדיר אותה.

שתי דרכים לקבל הודעה נכנסת

WebhookPolling
איך זה עובדSendy שולחת POST חתום לשרת שלך על כל הודעההקוד שלך קורא ל-GET /v1/inbound כל כמה דקות
מהירותשניות אחרי שההודעה נכנסהלפי תדירות הבדיקה
מה צריךכתובת HTTPS פומביתרק טוקן API
מתי מתאיםבוט, תור שירות, כל מה שצריך תגובה מהירהסקריפט לילי, סנכרון ל-CRM, סביבה בלי כתובת פומבית

אפשר גם להריץ את שתיהן יחד: webhook לזמן אמת, ו-polling פעם ביום כרשת ביטחון. מכיוון שלכל הודעה יש id קבוע, הכפילות לא מזיקה.

איך נראית הודעה נכנסת

זה האובייקט ש-GET /v1/inbound מחזיר בתוך data, ובדיוק אותו אובייקט מגיע בשדה data של אירוע ה-webhook:

{
  "id": "cmc0inbound0001",
  "from": "+972501234567",
  "to": "+972531234567",
  "text": "אשמח לקבל פרטים נוספים",
  "opt_out": "none",
  "opt_out_applied_at": null,
  "received_at": "2026-08-24T10:15:00.000Z"
}
  • from הוא השולח. כשמדובר בנייד ישראלי הוא מגיע בפורמט בינלאומי (+972...), אותו פורמט של אנשי הקשר, כך שאפשר לחפש אותו ישר עם GET /v1/contacts?phone=. כששולח הוא שם עסק או מספר מחו"ל, הערך מגיע כמו שהוא.
  • to הוא המספר הווירטואלי שלך.
  • opt_out הוא השדה שהכי חשוב לקרוא, וההסבר עליו מופיע בהמשך.

דרך 1: webhook, שלב אחר שלב

ההגדרה המלאה, כולל כל האירועים, מופיעה במדריך ה-Webhooks. בקצרה:

  1. נכנסים למפתחים ובוחרים בלשונית Webhooks.
  2. מוסיפים את כתובת ה-HTTPS שלך. יש נקודת קצה אחת לכל מרחב עבודה.
  3. מסמנים את האירוע inbound.received.
  4. מעתיקים את סוד החתימה למשתנה סביבה בשרת.
  5. לוחצים על שליחת אירוע בדיקה ומוודאים שהשרת מחזיר 200.

זה שרת Node.js מינימלי שמקבל את האירוע, מאמת שהוא באמת הגיע מ-Sendy ומעביר הלאה:

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const SECRET = process.env.SENDY_WEBHOOK_SECRET;

function isFromSendy(rawBody, header = "") {
  const t = header.match(/t=(\d+)/)?.[1];
  const sigs = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]);
  if (!t || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
  return sigs.some((s) => timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

app.post("/sendy/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!isFromSendy(rawBody, req.get("Sendy-Signature"))) return res.sendStatus(401);

  const event = JSON.parse(rawBody);
  res.sendStatus(200); // עונים מיד, מעבדים אחרי

  if (event.type === "inbound.received") {
    handleInbound(event.data).catch(console.error);
  }
});

app.listen(3000);

שלושה דברים שהקוד הזה עושה בכוונה:

  • מאמת על הגוף הגולמי. החתימה מחושבת על הבייטים שהתקבלו, ולכן express.raw ולא express.json. אחרי JSON.parse כבר אי אפשר לאמת.
  • מקבל שתי חתימות. אחרי החלפת סוד, הסוד הקודם ממשיך לחתום במקביל במשך 24 שעות, ולכן הבקשה יכולה לשאת שני ערכי v1.
  • עונה לפני שהוא מעבד. Sendy מחכה עד 10 שניות לתשובת 2xx. כתיבה ל-CRM יכולה לקחת יותר, אז היא קורית אחרי התשובה, או בתור עבודות.

דרך 2: polling ב-Python

אם אין לך כתובת פומבית, או שמספיק לך סנכרון כל כמה דקות, קוראים ל-GET /v1/inbound עם since. ההודעות חוזרות מהישנה לחדשה, ו-since כולל את הרגע עצמו, ולכן מסננים כפילויות לפי id:

import os
import time
from datetime import datetime, timedelta, timezone

import requests

API = "https://sendy.co.il/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SENDY_API_KEY']}"}


def fetch_inbound(since):
    params = {"since": since, "limit": 100}
    while True:
        r = requests.get(f"{API}/inbound", headers=HEADERS, params=params, timeout=10)
        r.raise_for_status()
        page = r.json()
        yield from page["data"]
        if not page["has_more"]:
            return
        params["cursor"] = page["next_cursor"]


seen = set()  # בפרודקשן: טבלה עם מפתח ייחודי על id

while True:
    since = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    for msg in fetch_inbound(since):
        if msg["id"] in seen:
            continue
        seen.add(msg["id"])
        handle_inbound(msg)
    time.sleep(300)

למה since ולא רק cursor: בעמוד האחרון next_cursor חוזר null, כך שאין סמן להמשיך ממנו בסבב הבא. חלון של יום אחורה עם סינון לפי id הוא הדרך הפשוטה שלא מפספסת הודעות. בדיקה כל חמש דקות רחוקה מאוד ממגבלת הקצב של 60 קריאות לדקה לטוקן.

מההודעה ל-CRM: מה עושים עם כל הודעה

כאן handle_inbound נכנסת לתמונה. השדה opt_out אומר לך איזה סוג הודעה קיבלת, ולכל סוג יש טיפול אחר:

opt_outמה זה אומרמה עושים ב-CRM
"detected"ההודעה היא בקשת הסרה, למשל "הסר". Sendy כבר הסירה את השולח, ו-opt_out_applied_at אומר מתימסמנים את איש הקשר כלא מעוניין בדיוור, כדי שה-CRM לא יחזיר אותו לרשימה בייבוא הבא
"suspected"מילת הסרה בתוך הודעה ארוכה יותר, כמו "אשמח שתסירו את החיוב". Sendy לא מסירה, ומסמנת את ההודעה לבדיקה במערכתפותחים משימה לאדם: זו יכולה להיות בקשת שירות ולא בקשת הסרה
"none"הודעה רגילה: שאלה, אישור, ליד, הצבעהמוצאים את איש הקשר לפי from, ורושמים פעילות, מעדכנים שלב או פותחים ליד

ככה זה נראה בקוד, עם פונקציות CRM כלליות שאתה מחליף בקריאות של המערכת שלך:

def handle_inbound(msg):
    phone, text = msg["from"], msg["text"]

    if msg["opt_out"] == "detected":
        crm.mark_unsubscribed(phone, at=msg["opt_out_applied_at"])
    elif msg["opt_out"] == "suspected":
        crm.create_task(phone, f"בדיקת בקשה אפשרית להסרה: {text}")
    else:
        crm.log_reply(phone, text, received_at=msg["received_at"])

לענות מהקוד. המספר הווירטואלי מאושר אוטומטית כשולח, כך שאפשר להשיב מאותו מספר שהלקוח כתב אליו. שולחים POST /v1/messages עם from שווה ל-to של ההודעה הנכנסת ו-to שווה ל-from שלה. תשובה לשאלה של לקוח היא הודעת שירות, ולכן כדאי לשלוח אותה במצב תפעולי.

טעויות נפוצות

  • להניח שכל הודעה מגיעה פעם אחת. שרת שלא ענה בזמן יקבל את אותו אירוע שוב, עד 8 ניסיונות לאורך כ-22 שעות. בודקים את ה-id לפני שכותבים, בדיוק כמו מפתח אידמפוטנטיות בשליחה.
  • להניח סדר. אירועים לא מגיעים בהכרח לפי הסדר. כשהסדר חשוב, ממיינים לפי received_at.
  • לחשוב שכל שורה היא הודעה שלמה. הודעה ארוכה במיוחד יכולה להגיע בכמה שורות נפרדות מאותו שולח, אחת אחרי השנייה.
  • לחכות להתראה מתיבת ההודעות. התיבה במערכת מקבלת בלבד ולא שולחת התראה על הודעה חדשה. ה-webhook הוא הדרך היחידה לקבל דחיפה.
  • לשחרר את המספר בלי גיבוי. אחרי שחרור, התיבה כבר לא מוצגת. מייצאים CSV לפני.

בלי לכתוב שרת: n8n, Make ו-Zapier

כל אחד מהכלים האלה נותן כתובת webhook נכנסת. מדביקים אותה כנקודת הקצה של Sendy, מסמנים inbound.received, וכל הודעה נכנסת מפעילה את התרחיש שלך. דרך שנייה היא צעד HTTP מתוזמן שקורא ל-GET /v1/inbound. ההבדל בין שתי הדרכים, ומה כל כלי יודע לעשות מול Sendy: חיבור Sendy לכלי אוטומציה.

שאלות נפוצות

אפשר לקבל SMS בלי מספר וירטואלי? לא דרך Sendy. משם עסק אי אפשר להשיב בכלל, ותשובות למספר הנייד שלך מגיעות לטלפון שלך ולא למערכת. המספר הווירטואלי הוא מה שמכניס את התשובות ל-API.

תשובת "הסר" מגיעה גם לקוד שלי? כן, עם opt_out שערכו "detected". ההסרה עצמה כבר קרתה ב-Sendy כשההודעה הגיעה, כך שהקוד שלך רק צריך לעדכן את ה-CRM.

אפשר לבחור את המספר? לא. המספר מוקצה מתוך מאגר, אחד לכל מרחב עבודה.

אפשר להשתמש ב-webhook וב-polling יחד? כן. שתי הדרכים מחזירות את אותו אובייקט עם אותו id, ומסננים כפילויות לפיו.

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


רוצה לחבר את התשובות למערכת שלך? התיעוד המלא של GET /v1/inbound ושל ה-Webhooks כולל את כל השדות והשגיאות. 30 ההודעות הראשונות חינם, בלי כרטיס אשראי.

רוצים לראות את זה בפעולה?

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

התחילו בחינם
→ חזרה לבלוג

אתר זה עושה שימוש בעוגיות (Cookies) ובכלים טכנולוגיים נוספים כדי לשפר את חוויית המשתמש, לנתח תנועה ולהתאים פרסומות. המשך הגלישה באתר מהווה את הסכמתך לכך ולמדיניות הפרטיות שלנו.