תיעוד API

ה-API שלך. המספר שלך. הכללים שלך.

חנות, CRM, מערכת תורים או סקריפט בן שמונה שורות — הקוד שלך שולח בקשה, וההודעה יוצאת מהמספר של העסק. מפתח אחד, HTTPS, JSON.

https://my.isayso.app/api-v1

בחמש דקות

מפיקים מפתח בדאשבורד: הגדרות ← מפתחות API ← מפתח חדש. המפתח מוצג פעם אחת — אנחנו שומרים רק גיבוב שלו, ואי אפשר לשחזר אותו אחר כך. אבד? מבטלים ומנפיקים חדש.

הקריאה הראשונה שכדאי לעשות — "המפתח עובד ומה מותר לו", במקום לגלות את זה מ-403 על פעולה אמיתית

# מי אני
curl https://my.isayso.app/api-v1/me \
  -H "Authorization: Bearer isk_live_..."

# → {"keyName":"החנות","scopes":["messages:send"],"orgId":4}

ומכאן — הודעה ראשונה

curl -X POST https://my.isayso.app/api-v1/messages \
  -H "Authorization: Bearer isk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"phone":"0501234567","body":"ההזמנה יצאה אליך 📦"}'

כתובת הבסיס: https://my.isayso.app/api-v1

כל הנתיבים בעמוד הזה יחסיים לכתובת הזו. HTTPS בלבד.

אבטחה

מפתח והרשאות

כל בקשה נושאת Authorization: Bearer <מפתח>. ההנחה שלנו היא שמפתח ידלוף — הוא יושב בקוד של מישהו אחר — ולכן ההרשאות צרות בכוונה.

אימות ב-Bearer

המפתח מתחיל ב-isk_live_, וזה מכוון: סורקי סודות של GitHub ו-GitLab מזהים דפוסים ידועים. מפתח שדולף ומזוהה מיד עדיף על מפתח שדולף ואיש לא יודע.

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

ההרשאות פר-מפתח

מפתח שרק שולח אישורי הזמנה לא צריך לקרוא את אנשי הקשר שלך, וכשהוא ידלוף — הנזק מוגבל למה שסימנת:

  • messages:send שליחת הודעות
  • messages:read קריאת סטטוס הודעות
  • contacts:read קריאת אנשי קשר ורשימת הסרות
  • contacts:write יצירה ועדכון של אנשי קשר
  • optouts:write רישום הסרה
  • events:read קריאת הודעות נכנסות

מה שאין ב-API הזה — בכוונה

אין כאן claim, report, inbound (של מכשירים) או heartbeat. פעולות המכשיר המצומד שמורות למכשיר בלבד: מי שיכול לתבוע הודעה מושך אליו הודעות אמיתיות של לקוחות ומונע מהטלפון לשלוח אותן, ומי שיכול לדווח מסמן הודעה כ"נשלחה" בלי שיצאה. מפתח API שדלף — גם ביד הלא נכונה — לא יכול לעשות את זה. היכולות האלה פשוט אינן קיימות בו.

הנתיבים

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

GET/api-v1/meהמפתח עובד ומה מותר לו
POST/api-v1/messagesשליחת הודעה
messages:send
GET/api-v1/messages/:idסטטוס הודעה
messages:read
GET/api-v1/messages?since=&status=&limit=רשימת הודעות — limit עד 100, ברירת מחדל 50
messages:read
GET/api-v1/inbound?since=&limit=תשובות נכנסות, במשיכה
events:read
GET/api-v1/contacts?limit=רשימת אנשי קשר
contacts:read
POST/api-v1/contactsupsert לפי מספר טלפון
contacts:write
POST/api-v1/opt-outsרישום הסרה
optouts:write
GET/api-v1/opt-outsרשימת ההסרות
contacts:read

מפתח אידמפוטנטיות — ההגנה החשובה כאן

תקלת רשת גורמת לאינטגרציה לנסות שוב, והלקוח מקבל את אותה הודעה פעמיים. אין דרך לבטל SMS שיצא. עם idempotencyKey (4–120 תווים) השנייה נבלעת בשקט. השתמשו במזהה שכבר יש לכם — מספר הזמנה, מזהה תור:

{
  "phone": "0501234567",
  "body": "התור שלך מחר ב-10:00",
  "kind": "transactional",
  "sendAt": null,
  "idempotencyKey": "appointment-8842"
}

phone מקומי (0501234567) או בינלאומי (+972501234567); body עד 1200 תווים, עברית ואמוג'י נתמכים; kind הוא transactional (ברירת מחדל) או campaign; sendAt ב-ISO 8601, חייב להיות בעתיד.

שתי תשובות אפשריות, ושתיהן הצלחה

// 201 — נכנסה לתור
{ "id": 918, "status": "queued",
  "phone": "972501234567", "scheduledAt": null }

// 200 — נבלעה: כפילות, או שהנמען הסיר את עצמו
{ "ok": true, "duplicate": true, "message": "..." }

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

phone חוזר מנורמל, בלי +. זו הצורה הקנונית בכל המערכת — אם אתם משווים מחרוזות אצלכם, השוו מולה.

מסלול חיי ההודעה: queued → claimed → sent → delivered, ולצידם failed, cancelled, expired. scheduled מופיע כשנתתם sendAt.

אין "נקרא"

לפרוטוקול ה-SMS אין אישור קריאה — לא אצלנו ולא אצל אף אחד. delivered פירושו שהרשת אישרה מסירה. מי שמבטיח לכם אחוזי קריאה על SMS מוכר לכם מספר שאין לו מקור. הפרוקסי האמיתי הוא הקלקות: קישורים בגוף ההודעה מקוצרים אוטומטית, וההקלקות משויכות לנמען.

תשובות נכנסות, במשיכה

מי שיש לו שרת עם כתובת ציבורית — עדיף שישתמש ב-Webhook. לתוסף בחנות מקומית לרוב אין כזה, ואז מושכים מ-GET /api-v1/inbound:

{
  "inbound": [
    { "id": 51, "phone": "972501234567",
      "body": "מעולה תודה", "receivedAt": "...",
      "triggeredOptOut": false,
      "contactId": 12, "conversationId": 7 }
  ],
  "cursor": 51
}

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

triggeredOptOut: true אומר שהטקסט טופל כבקשת הסרה. סמנו את זה גם אצלכם — מכאן והלאה לא נשלח אליו דיוור, וכדאי שהמערכת שלכם תדע למה.

אנשי קשר והסרות

ה-POST /api-v1/contacts הוא upsert לפי מספר טלפון: מספר שקיים מתעדכן, ואינכם צריכים לבדוק קודם.

{ "phone": "0501234567", "firstName": "דנה",
  "lastName": "לוי", "email": "dana@example.com",
  "marketingConsent": true }

// → 201 {"id":88,"created":true} · 200 {"id":88,"created":false}

marketingConsent הוא הצהרה משפטית ולא נוחות. סמנו true רק כשיש לכם הסכמה אמיתית ומתועדת לדיוור.

הסרות — הנתיב הזה קיים בשביל הצד החוקי. לקוח שלחץ "הסר אותי" באתר שלכם חייב להיות מוסר גם אצלנו (POST /api-v1/opt-outs). בלי הקריאה הזו ההסרה נשארת רק אצלכם — והדיוור הבא שלנו יוצא אליו בכל זאת, בשמכם. הכיוון ההפוך עובד מעצמו: מי שמשיב "הסר" ב-SMS נרשם אצלנו אוטומטית.

אירועים

Webhooks

נרשמים בדאשבורד: הגדרות ← מפתחות API ← Webhooks. שני אירועים: inbound.received — לקוח השיב להודעה, ו-message.status — סטטוס הודעה השתנה.

מטען האירועים

// inbound.received
{ "phone": "972501234567", "body": "מעולה תודה",
  "receivedAt": "...", "triggeredOptOut": false,
  "contactId": 12, "conversationId": 7 }

// message.status
{ "id": 918, "phone": "972501234567",
  "status": "delivered", "error": null, "at": "..." }

כותרות וניסיונות חוזרים

  • X-Isayso-Event שם האירוע
  • X-Isayso-Delivery מזהה מסירה. סננו כפילות לפיו, לא לפי תוכן
  • X-Isayso-Signature החתימה

כל תשובה 2xx נחשבת הצלחה. אחרת: ניסיון חוזר אחרי 1, 5, 20, 60, 180 דקות. אחרי 20 כשלים רצופים ה-webhook מושהה אוטומטית ומופיע כך בדאשבורד — שרת שנפל לפני חצי שנה לא ימשיך לקבל ניסיונות לנצח.

ה-timeout הוא 10 שניות. ענו מיד ועבדו ברקע: עיבוד ארוך בתוך הבקשה ייספר ככישלון גם אם הצליח אצלכם.

אימות החתימה — Node.js

X-Isayso-Signature: t=1785759586,v1=5a1f...c9

const [t, v1] = header.split(',').map(p => p.split('=')[1]);

const expected = crypto.createHmac('sha256', secret)
  .update(`${t}.${rawBody}`).digest('hex');

// השוואה בזמן קבוע — `===` דולף מידע דרך זמן הריצה
const ok = crypto.timingSafeEqual(
  Buffer.from(v1), Buffer.from(expected));

// ודאו גם שה-timestamp טרי (חמש דקות), אחרת שידור
// חוזר של בקשה ישנה שנלכדה עדיין ייחשב תקף.
const fresh = Math.abs(Date.now()/1000 - Number(t)) < 300;

PHP

[$t, $v1] = array_map(
  fn($p) => explode('=', $p)[1],
  explode(',', $header));

$expected = hash_hmac('sha256', "$t.$rawBody", $secret);
$ok = hash_equals($expected, $v1);

הגוף הגולמי, לפני פענוח JSON

ה-HMAC-SHA256 מחושב על <timestamp>.<גוף גולמי> עם סוד ה-webhook. פענוח ובנייה מחדש משנים רווחים וסדר מפתחות — והחתימה לא תתאים.

שגיאות

צורה אחת לכל החטיבה, כדי שהאינטגרציה תטפל בה פעם אחת.

צורת השגיאה

{ "error": "bad_phone",
  "message": "מספר הטלפון אינו תקין" }

401 ו-403 נפרדים בכוונה

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

הקודים

  • 400 bad_request · bad_phone — לתקן את הבקשה. ניסיון חוזר זהה ייכשל שוב
  • 401 unauthorized — מפתח חסר, שגוי או שבוטל
  • 402 plugin_required — הגישה ל-API אינה כלולה במנוי
  • 403 insufficient_scope — המפתח תקף, ההרשאה חסרה
  • 404 not_found — הפריט אינו קיים בחשבון שלך
  • 429 too_many_attempts — יותר מדי ניסיונות עם מפתח שגוי מאותה כתובת
חשוב לדעת

מה נאכף תמיד

מפתח API אינו עוקף דבר. כל הודעה שנכנסת דרכו עוברת באותה נקודה שכל הודעה במערכת עוברת בה:

  • הסרות — נמען שהסיר את עצמו לא יקבל, ותקבלו duplicate
  • מכסה — הודעות דרך ה-API נספרות במנוי כמו כל השאר
  • ויסות ותקרות — המרווח בין הודעות ותקרות יומיות מגנים על ה-SIM מחסימה. הודעה בהולה נכנסת לתור, לא עוקפת אותו
  • שעות שקט — מה שהגדרתם בדאשבורד חל גם כאן
  • שורת המיתוג — מתווספת אלא אם רכשתם white-label
  • קיצור לינקים — קישורים בגוף מקוצרים אוטומטית, וההקלקות משויכות לנמען

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

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

מתחילים לשלוח בקוד?

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