API ל-SMS
שליחה פרוגרמטית של SMS בודד או מרובה, עם החלפת ערכים דינמית לכל נמען. בדיקת מצב של שליחות מרובות, קריאת לוג ההודעות, מענה במקום.
אימות והרשאות
כל endpoint נדרש Bearer. שתי הרשאות ייעודיות ל-SMS:
sms:send— נדרש לשליחה (בודדת, מרובה, מענה)sms:read— נדרש לקריאת לוג ההודעות, היסטוריה ותוצאות שליחה מרובה
ערכת ההרשאות החדשה למשתמש כוללת את שתיהן. אימות מספר הטלפון בחשבון נדרש לפני שליחה (אחרת מוחזר SMS_PHONE_NOT_VERIFIED).
שליחה בודדת או מרובה — POST /api/v1/sms
endpoint יחיד מטפל בשני המצבים. הנתיב הבודד מחזיר תשובה סינכרונית; שליחה מרובה מחזירה 202 Accepted עם jobId לבדיקת מצב.
מבנה הגוף: { from, body, recipients: [{ phone, variables? }] }. שדה דינמי שלא נפתר ({{name}} חסר) חוסם את כל השליחה ומוחזר עם שם המפתח החסר ומספר הנמען.
בדיקת שליחות מרובות — GET /api/v1/sms/jobs/:id
מחזיר את המצב הנוכחי של השליחה המרובה (queued / processing / completed / partial / failed) ורשימת נמענים עם מצב מסירה, מספר מקטעים ומחיר לכל אחד.
לוג הודעות + מענה
שלושה endpoints למשטח לוג ההודעות:
GET /api/v1/sms/threads— רשימת לוג ההודעותGET /api/v1/sms/threads/:id— היסטוריה מלאה (עם pagination)POST /api/v1/sms/threads/:id/reply— שליחת מענה (מ/אל נגזרים מהלוג)
סימון לוג כנקרא: PATCH /api/v1/sms/threads/:id עם גוף { "markRead": true }.
מגבלות וקודי שגיאה
כל שגיאה מוחזרת במעטפת המשותפת { ok: false, error: { code, message, ... }, requestId }. קודי שגיאה ייעודיים ל-SMS (עם סטטוס HTTP):
SMS_PHONE_NOT_VERIFIED— 412SMS_NO_OWNED_NUMBER— 404SMS_FREE_QUOTA_EXCEEDED/SMS_PAYING_QUOTA_EXCEEDED— 429SMS_INSUFFICIENT_BALANCE— 402SMS_GEO_BLOCKED— 403SMS_INVALID_E164/SMS_MISSING_VARIABLE/SMS_BODY_TOO_LONG— 400SMS_BATCH_TOO_LARGE— 413
שליחת SMS בודד
POST /api/v1/sms עם נמען אחד ב-recipients מחזיר תשובה סינכרונית עם מזהה ההודעה, מזהה השיחה והעלות לפי מקטעים.
- שדה
fromחייב להיות מספר שיש עליו בעלות — אפשר לרכוש מספר ב-/dashboard/agents. - שדה
bodyהוא מחרוזת רגילה. עברית או ערבית מעבירות אוטומטית לקידוד UCS-2 (כ-70 תווים למקטע במקום 160 ב-GSM-7). - העלות יורדת מהיתרה לפני השליחה לספק. שליחה שנכשלת מקבלת החזר אוטומטי.
- דורש את ההרשאה
sms:send.
import os
import httpx
API_BASE = "https://aicall.co.il/api/v1"
API_KEY = os.environ["API_KEY"]
def send_sms(from_number: str, to: str, body: str) -> dict:
res = httpx.post(
f"{API_BASE}/sms",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"from": from_number,
"body": body,
"recipients": [{"phone": to}],
},
timeout=30,
)
res.raise_for_status()
return res.json()["data"]
if __name__ == "__main__":
result = send_sms(
from_number="+972501111111",
to="+972502222222",
body="Hi! Your appointment is confirmed for tomorrow at 10:00.",
)
print(f"Sent message #{result['messageId']} in thread {result['threadId']}")
print(f" {result['segments']} segment(s), {result['encoding']}")
print(f" Charged: ${result['priceUsd']:.4f}")קודי שגיאה נפוצים:
SMS_PHONE_NOT_VERIFIED(412) — אימות מספר הטלפון האישי תחת/dashboard/settings.SMS_INVALID_E164(400) — מספר הנמען חייב להיות בפורמט בינלאומי כמו972501234567+.SMS_GEO_BLOCKED(403) — מדינת היעד לא נמצאת ברשימה המאופשרת (ברירת מחדל: IL ו-US).SMS_INSUFFICIENT_BALANCE(402) — טעינת יתרה כדי להמשיך.SMS_FREE_QUOTA_EXCEEDED/SMS_PAYING_QUOTA_EXCEEDED(429) — נגמרה המכסה היומית.
שליחת הודעות מרובה עם שדות דינמיים
אותו endpoint מטפל גם בשליחה מרובה — מעבירים יותר מנמען אחד ב-recipients, ואפשר לצרף לכל נמען אובייקט variables שממלא את {{placeholders}} בגוף ההודעה.
- מחזיר
202 AcceptedעםjobId— אפשר לבדוק תוצאות לכל נמען ב-/api/v1/sms/jobs/:id. - החלפת המשתנים נוקשה — שדה חסר חוסם את כל השליחה והתשובה מציינת איזה נמען ואיזה שדה חסר.
- גודל ברירת מחדל לשליחה מרובה: 500 (ניתן לכיוון על ידי האדמין).
- כל נמען מחויב בנפרד — סה״כ המחיר = סכום המחירים לכל נמען.
import os
import httpx
API_BASE = "https://aicall.co.il/api/v1"
API_KEY = os.environ["API_KEY"]
def send_batch(from_number: str, body_template: str, recipients: list[dict]) -> dict:
res = httpx.post(
f"{API_BASE}/sms",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"from": from_number,
"body": body_template,
"recipients": recipients,
},
timeout=30,
)
res.raise_for_status()
return res.json()["data"]
if __name__ == "__main__":
result = send_batch(
from_number="+972501111111",
body_template="Hi {{name}}, your link is {{link}}",
recipients=[
{"phone": "+972502222222", "variables": {"name": "Dana", "link": "https://example.com/d"}},
{"phone": "+972503333333", "variables": {"name": "Avi", "link": "https://example.com/a"}},
{"phone": "+972504444444", "variables": {"name": "Noa", "link": "https://example.com/n"}},
],
)
print(f"Queued batch job #{result['jobId']}")
print(f" {result['recipientCount']} recipients")
print(f" Estimated cost: ${result['estimatedPriceUsd']:.4f}")
print(f" Per country: {result['perCountry']}")טיפים לשליחות מרובות בייצור:
- כדאי לשמור מפת זיכרון
jobId → metadataבזמן שהמשימה רצה כדי לקשר תוצאות חזרה לשורות ה-CSV או למסד הנתונים. - המעבד מאט אוטומטית: 4 הודעות בשנייה ליעדי US, אחת בשנייה לישראל — שליחה מרובה של 200 ל-US לוקחת כ-50 שניות.
- נמענים בעברית/ערבית עולים פי 3 לתו (UCS-2 לעומת GSM-7). התצוגה המקדימה מחזירה מספר מקטעים לכל נמען כדי שאפשר יהיה להציג זאת לפני השליחה.
מעקב אחר שליחה מרובה עד לסיום
GET /api/v1/sms/jobs/:id מחזיר את המצב הנוכחי ורשימת תוצאות לכל נמען — קצב מומלץ: קריאה כל 2 שניות.
- מעברי מצב:
queued → processing → completed(אוpartialאם חלק מהנמענים נכשלו, אוfailedאם כולם). - שורת נמען מכילה
status,segments,encoding,priceUsd,priceIls, ובכישלון גםerrorCodeו-errorMessage. - דורש את ההרשאה
sms:read.
import os
import time
import httpx
API_BASE = "https://aicall.co.il/api/v1"
API_KEY = os.environ["API_KEY"]
POLL_SEC = 2
def wait_for_batch(job_id: int) -> dict:
while True:
res = httpx.get(
f"{API_BASE}/sms/jobs/{job_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
res.raise_for_status()
job = res.json()["data"]
terminal = job["status"] in ("completed", "failed", "partial")
print(f" [{job['status']}] {job['sentCount']}/{job['totalRecipients']} sent, {job['failedCount']} failed")
if terminal:
return job
time.sleep(POLL_SEC)
if __name__ == "__main__":
job_id = int(os.environ.get("JOB_ID", "0"))
job = wait_for_batch(job_id)
print(f"Batch #{job_id} finished as: {job['status']}")
for r in job["recipients"]:
if r["status"] in ("sent", "delivered"):
print(f" ✓ #{r['id']} ${float(r['priceUsd']):.4f} {r['segments']}s")
else:
print(f" ✗ #{r['id']} {r.get('errorMessage') or r['status']}")ה-cron ב-/api/cron/refresh-sms-status משלים את מצב המסירה כל 5 דקות, כך שורה שתקועה ב-sending מעל כ-2 דקות תתעדכן ל-delivered או ל-failed בסופו של דבר גם אם קריאת ה-status callback של הספק התפספסה.
רשימת לוג ההודעות ומענה ללוג
תגובות נכנסות מגיעות ללוג ההודעות — לוג אחד לכל זוג (המספר שלך, מספר הצד השני). שלושה endpoints מכסים את המשטח:
GET /api/v1/sms/threads— רשימת לוג ההודעות לפי פעילות אחרונה. תומך ב-?limit,?offset,?ownedNumberId.GET /api/v1/sms/threads/:id— היסטוריית הודעות מלאה (מהחדשה לישנה דרך?before=<cursor messageId>).POST /api/v1/sms/threads/:id/reply— שליחת מענה; הכתובות נגזרות מהלוג.
סימון לוג כנקרא: PATCH /api/v1/sms/threads/:id עם גוף { "markRead": true }.
import os
import httpx
API_BASE = "https://aicall.co.il/api/v1"
API_KEY = os.environ["API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def list_threads(limit: int = 50) -> list[dict]:
res = httpx.get(f"{API_BASE}/sms/threads", headers=HEADERS, params={"limit": limit}, timeout=15)
res.raise_for_status()
return res.json()["data"]["threads"]
def read_thread(thread_id: int) -> dict:
res = httpx.get(f"{API_BASE}/sms/threads/{thread_id}", headers=HEADERS, timeout=15)
res.raise_for_status()
return res.json()["data"]
def reply(thread_id: int, body: str) -> dict:
res = httpx.post(
f"{API_BASE}/sms/threads/{thread_id}/reply",
headers={**HEADERS, "Content-Type": "application/json"},
json={"body": body},
timeout=30,
)
res.raise_for_status()
return res.json()["data"]
if __name__ == "__main__":
# Show open conversations and auto-reply to anything new from a known contact.
for th in list_threads(limit=20):
marker = " (NEW)" if th["unreadCount"] > 0 else ""
print(f"#{th['id']} {th['remotePhone']} via {th['ownedNumber']}{marker}")
if th["unreadCount"] > 0 and th["remotePhone"] == "+972509999999":
sent = reply(th["id"], "Got it — I'll get back to you within the hour.")
print(f" → auto-replied as message #{sent['messageId']}")תגובות נספרות באותה מכסה יומית כמו שליחות ישירות. תגובה שמחזירה SMS_FREE_QUOTA_EXCEEDED פירושה שהמכסה היומית נגמרה — ההודעה נדחית לפני קריאה לספק.
