שליחת קוד אימות ב-SMS היא קריאת POST אחת עם מספר הנמען, שם שולח מאושר וגוף ההודעה. מה שהופך אותה לאמינה הוא מה שסביבה: תוקף קצר, הגבלת קצב לכל משתמש, מפתח idempotency שמונע שליחה כפולה בריטריי, וחיווי מסירה שמאשר שההודעה הגיעה למכשיר.
מה צריך לפני הקריאה הראשונה
שלושה דברים, וכולם חד-פעמיים:
טוקן API. נוצר בפאנל תחת Developers. נשלח כ-Authorization: Bearer.
אם עדיין לא בחרתם ספק, שמונה השאלות במדריך לבחירת ספק SMS API מפרקות כל מחירון ותיעוד.
שם שולח מאושר. אי אפשר לשלוח מטקסט שרירותי. השולח חייב להיות מאושר מראש בחשבון, וקריאה עם שולח לא מאושר נדחית. אפשר למשוך את הרשימה מ-GET /api/v1/senders.
יתרת קרדיט. GET /api/v1/credits מחזיר את היתרה. שליחה בלי יתרה נדחית לפני שיוצאת.
קריאת ה-API
curl -X POST https://sendy.co.il/api/v1/messages \
-H "Authorization: Bearer $SENDY_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-user-8412-1786033878" \
-d '{
"to": "+972501234567",
"from": "MyApp",
"body": "קוד האימות שלך: 481920. תקף ל-5 דקות.",
"mode": "transactional"
}'
התשובה, 202 לשליחה חדשה:
{
"id": "cmsg1bxbq001cpm012b6tld00",
"status": "queued",
"credits_charged": 1,
"credits_refunded": 0
}
שים לב ש-Idempotency-Key הוא header ולא שדה בגוף. זו טעות נפוצה, והיא שקטה: הבקשה תעבור, פשוט בלי הגנה מפני כפילות.
ב-Node:
const res = await fetch("https://sendy.co.il/api/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDY_API_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": `otp-${userId}-${Date.now()}`,
},
body: JSON.stringify({
to: phone,
from: "MyApp",
body: `קוד האימות שלך: ${code}. תקף ל-5 דקות.`,
mode: "transactional",
}),
});
const { id, status, credits_charged } = await res.json();
וב-Python:
import requests, time
r = requests.post(
"https://sendy.co.il/api/v1/messages",
headers={
"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": f"otp-{user_id}-{int(time.time())}",
},
json={
"to": phone,
"from": "MyApp",
"body": f"קוד האימות שלך: {code}. תקף ל-5 דקות.",
"mode": "transactional",
},
)
r.raise_for_status()
message_id = r.json()["id"]
למה קוד אימות הוא transactional, ולא marketing
זה השדה שהכי קל להתעלם ממנו וגם היחיד שיכול לשבור לך את המוצר.
mode מצהיר על מטרת ההודעה, ויש לו שתי השלכות שונות לגמרי:
| mode | מה קורה |
|---|---|
marketing | שליחה למי שביקש להסיר את עצמו נדחית, ולא מחויבת |
transactional | ההצהרה שלך שזו לא הודעה שיווקית. בדיקת ההסרה לא חלה |
קוד אימות הוא הודעה תפעולית שהמשתמש ביקש בעצם ההתחברות, ולכן transactional הוא הסיווג הנכון עבורו. וזו לא פרצה, זו התנהגות רצויה: משתמש שהסיר את עצמו מהדיוור השיווקי שלך עדיין חייב לקבל את קוד ההתחברות. מערכת שתחסום אותו תנעל אותו מחוץ למוצר.
הכיוון ההפוך חשוב באותה מידה. אם תשלח דיוור שיווקי ב-transactional כדי לעקוף הסרות, הצהרת הצהרה שקרית והיא נרשמת. ההבחנה בין הודעה תפעולית לדבר פרסומת היא הבחנה משפטית ולא טכנית, ופירטנו אותה במאמר על מה נחשב דבר פרסומת ובתוך המדריך לחוק הספאם לעסקים.
אם לא תשלח mode בכלל, ההגדרה ברמת החשבון מכריעה. בחשבון חדש ברירת המחדל היא marketing, כלומר בדיקת ההסרה פעילה. עדיף לשלוח את השדה במפורש בקוד ולא להסתמך על הגדרה שמישהו יכול לשנות בפאנל.
תוקף, אורך הקוד ומספר ניסיונות
אלה החלטות מימוש בצד שלך, לא פיצ'רים של ה-API, אבל הן מה שמפריד בין OTP עובד ל-OTP פריץ:
- תוקף 5 עד 10 דקות. ארוך יותר מרחיב את חלון ההתקפה בלי להוסיף נוחות אמיתית.
- 6 ספרות הוא הסטנדרט. פחות מזה ניתן לניחוש, יותר מזה מעצבן להקליד.
- תקרת ניסיונות אימות, בערך 5, ואז הקוד נשרף. בלי זה קוד בן 6 ספרות נשבר בכוח גס.
- הגבלת בקשות לכל משתמש, לא רק גלובלית. אחרת מישהו יכול להזרים בקשות קוד למספר של אדם אחר.
- לשמור hash של הקוד, לא את הקוד עצמו.
הגבלת קצב
ה-API מוגבל ל-60 בקשות לדקה לטוקן. חריגה מחזירה 429 עם RATE_LIMITED.
if (res.status === 429) {
// backoff אקספוננציאלי, לא ריטריי מיידי
await sleep(Math.min(2 ** attempt * 500, 30000));
}
בפועל, 60 לדקה מספיק לרוב מוצרי ה-OTP. אם אתה מתקרב לתקרה, בדוק קודם שאין לך לולאת ריטריי שמייצרת את העומס בעצמה.
מניעת שליחה כפולה
הודעת SMS שיצאה אי אפשר להחזיר, ולכן ריטריי תמים הוא בעיה אמיתית: המשתמש מקבל שני קודים שונים, מזין את הראשון, ונכשל.
Idempotency-Key פותר את זה. בקשה חוזרת עם אותו מפתח מחזירה את התוצאה המקורית במקום לשלוח שוב - ותקבל 200 במקום 202, כך שאפשר להבחין בקוד בין שליחה חדשה לשחזור.
המערכת גם קושרת את המפתח לתוכן ההודעה, כך שאם תשלח את אותו מפתח עם גוף אחר תקבל שגיאת התנגשות במקום שקט מטעה.
מפתח טוב הוא כזה שנגזר מהפעולה ולא מהזמן: otp-{user_id}-{attempt_id} עדיף על חותמת זמן, כי ריטריי אחרי שנייה צריך לקבל את אותו מפתח.
השגיאות שתפגוש, ומה לעשות עם כל אחת
זה החלק שקובע אם המימוש שלך שורד את היום הראשון בפרודקשן. כל שגיאה חוזרת עם קוד יציב שאפשר לבדוק מולו בקוד, במקום לנתח מחרוזות.
| קוד | HTTP | מה קרה | מה לעשות |
|---|---|---|---|
INVALID_PHONE | 400 | המספר לא נורמליזבילי | לוודא פורמט לפני השליחה, לא אחריה |
INVALID_RECIPIENT | 400 | הנמען נדחה | להציג שגיאה למשתמש. ריטריי לא יעזור |
MESSAGE_TOO_LONG | 400 | מעל תקרת הגוף | לא רלוונטי ל-OTP, אבל תופס תבנית שהשתבשה |
INVALID_API_KEY | 401 | טוקן שגוי או מבוטל | לא לנסות שוב. להתריע |
INSUFFICIENT_CREDITS | 402 | אין יתרה | להתריע לצוות, לא למשתמש. כאן התחברויות מפסיקות לעבוד |
SENDER_NOT_APPROVED | 403 | השולח לא אושר | תקלת הגדרה, לא זמן ריצה |
RECIPIENT_UNSUBSCRIBED | 403 | שליחה ב-marketing למי שהוסר | אם זה OTP, שלחת ב-mode הלא נכון |
IDEMPOTENCY_CONFLICT | 409 | אותו מפתח, גוף אחר | באג אצלך. שני קודים שונים תחת מפתח אחד |
RATE_LIMITED | 429 | מעל 60 לדקה | backoff |
שתי שורות בטבלה הזו שוות תשומת לב מיוחדת.
INSUFFICIENT_CREDITS הוא כשל תפעולי שנראה כמו כשל משתמש. ברגע שהיתרה נגמרת, אף אחד לא מצליח להתחבר, והמשתמשים ידווחו על "האתר שבור" ולא על "לא קיבלתי קוד". שווה התראה על יתרה נמוכה לפני שמגיעים לשם.
RECIPIENT_UNSUBSCRIBED על הודעת OTP הוא סימן מובהק שה-mode שגוי. אם הוא מופיע, אתה שולח קודי אימות כדיוור שיווקי, ומשתמשים שהסירו את עצמם נעולים מחוץ לחשבון.
חיווי מסירה: ההבדל בין נשלח לנמסר
זה החלק שרוב המימושים מדלגים עליו, וזה בדיוק החלק שעונה על "המשתמש אומר שלא קיבל".
הודעה עוברת דרך רצף מצבים: queued ← sending ← sent ← delivered. או undelivered / failed.
sent אומר שההודעה יצאה מהמערכת. delivered אומר שהיא הגיעה למכשיר. אלה שני דברים שונים, והפער ביניהם הוא בדיוק המקום שבו תלונות נולדות.
curl https://sendy.co.il/api/v1/messages/{id} \
-H "Authorization: Bearer $SENDY_API_TOKEN"
כל שינוי מצב נכתב עם חותמת זמן ונשמר. המשמעות המעשית: כשלקוח פותח קריאה ואומר שלא קיבל קוד, יש לך תשובה מתוארכת ולא ניחוש. אתה רואה אם ההודעה נמסרה, מתי, או אם היא נכשלה ולמה.
כמה זה עולה
קרדיט אחד לכל הודעה, עד 201 תווים. הודעת OTP טיפוסית היא הרבה מתחת לזה, אז היא קרדיט אחד.
החיוב אחיד ולא תלוי בשפה: 201 תווים הם קרדיט אחד, בעברית כמו באנגלית. אין ספירה נפרדת ואין הפתעות בחשבונית.
הודעת OTP טיפוסית היא כמה עשרות תווים, אז היא תמיד קרדיט אחד. אם אתה בונה תבנית ארוכה יותר, ספור 201 ותדע כמה תשלם.
שאלות נפוצות
איך שולחים קוד אימות ב-SMS מהקוד?
קריאת POST אחת ל-/api/v1/messages עם to, from, body ו-mode, וטוקן ב-Authorization. הוסף Idempotency-Key כדי שריטריי לא ישלח פעמיים.
מה צריך להיות ה-mode של הודעת OTP?
transactional. קוד אימות הוא הודעה תפעולית ולא דיוור פרסומי, ולכן בדיקת ההסרה לא צריכה לחול עליו - אחרת משתמש שהסיר את עצמו מהשיווק לא יוכל להתחבר.
כמה זמן קוד אימות צריך להיות תקף? 5 עד 10 דקות, עם תקרה של כ-5 ניסיונות אימות לפני שהקוד נשרף.
איך יודעים שהקוד באמת הגיע?
לפי חיווי המסירה. sent אומר שההודעה יצאה, delivered אומר שהיא הגיעה למכשיר. שאילתה על GET /api/v1/messages/{id} מחזירה את המצב הנוכחי.
מה קורה אם שולחים את אותה בקשה פעמיים?
עם אותו Idempotency-Key, השנייה מחזירה את התוצאה המקורית ב-200 במקום לשלוח שוב. בלי מפתח, יישלחו שתי הודעות ויחויבו שני קרדיטים.
כמה בקשות לדקה מותרות?
60 לטוקן. חריגה מחזירה 429.
רוצה לבדוק? 30 ההודעות הראשונות חינם, בלי כרטיס אשראי. הקריאה הראשונה לוקחת פחות מדקה.
