ThunderPhone 2.0 באוויר.בשירות עצמי, החל מ-2¢/דקה.קראו את הודעת ההשקה

Developer cookbook

הגדרה דינמית לכל שיחה

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

כברירת מחדל, לכל מספר טלפון ולכל מפתח לפרסום מוקצה סוכן סטטי. כאשר נדרשת התאמה אישית לכל מתקשר או לכל מבקר — ניתוב VIP, הקשר של משתמש מחובר, בדיקות הנחיה מסוג A/B — עברו למצב וובהוק ותנו לשרת שלכם להחליט.

איך זה עובד

  1. אתם נרשמים לאירוע telephony.incoming (טלפון) או web.incoming (ווידג'ט). שניהם וובהוקים חוסמים: ThunderPhone ממתינה לתשובתכם לפני המשך השיחה, עד 13 שניות עבור שיחת טלפון (כולל ניסיון חוזר אחד) ו-10 שניות עבור סשן וידג'ט.
  2. ThunderPhone שולחת לכם {call_id, from_number, to_number} (סשני וידג'ט כוללים שדות ייעודיים לווידג'ט במקום מספרים — ראו את סכמת הבקשה).
  3. השרת שלכם מחזיר תצורת סוכן (הנחיה, קול, מוצר, כלים). ThunderPhone משתמשת בתצורה זו עבור השיחה.
  4. אין סוכן גיבוי. אם אתם מחזירים {} או תצורה לא תקפה, או שהבקשה עדיין נכשלת לאחר הניסיון החוזר שלה, שיחת הטלפון מסתיימת מיד ונרשמת עם end_reason inbound_config_webhook_failed; בקשת סשן וידג'ט נכשלת.

1. הגדירו את יעד הוובהוק

עבור מספרי טלפון, הירשמו עם נקודת הקצה שלכם אל telephony.incoming:

curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label":  "Prod call-incoming",
    "url":    "https://example.com/thunderphone/incoming",
    "events": ["telephony.incoming"]
  }'

התשובה כוללת secret חד-פעמי — שמרו אותו; תשתמשו בו לאימות חתימה.

עבור מפגשי וידג'ט, צרו מפתח לפרסום ב-mode="webhook" שכתובת נקודת הקצה שלכם כלולה בו:

curl -X POST https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":            "Dynamic widget",
    "mode":            "webhook",
    "webhook_url":     "https://example.com/thunderphone/widget-incoming",
    "allowed_domains": ["example.com"]
  }'

הווידג'ט ישלח בקשת POST לכתובת זו בתחילת כל מפגש.

2. מימוש המטפל

שלושה כללי אצבע:

  • אמתו את החתימה בכל בקשה (ראו אימות חתימות webhook). אל תדלגו על כך בסביבת פיתוח — הגדירו זאת נכון פעם אחת והשתמשו בו מחדש.
  • השיבו במהירות. הניסיון הראשון של שיחת טלפון נקטע לאחר 10 שניות, וכל האינטראקציה לאחר 13 שניות, וכל שנייה היא שקט מת עבור המתקשרים. בצעו חיפושים במסד הנתונים אם נדרש, אך אל תקראו למודלי LLM במורד הזרם באופן סינכרוני — אם אתם רוצים יצירת הנחיה דינמית, חשבו מראש ושמרו במטמון.
  • בצעו חזרה נקייה לברירת מחדל. בכל מצב בלתי צפוי, השיבו עם prompt ו-voice המוגדרים כברירת מחדל (או עם agent_id שמור). החזרת {} או סטטוס שגיאה מסיימת את השיחה.
FastAPI
import hashlib
import hmac
import json
import os
 
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
 
def verify(body: bytes, sig: str) -> bool:
    expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig or "")
 
@app.post("/thunderphone/incoming")
async def incoming(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(401)
 
    event = json.loads(body)
    if event["type"] not in ("telephony.incoming", "web.incoming"):
        return {}  # fall back to default
 
    caller = event["data"]["from_number"]
    # Cheap DB lookup: is this a known VIP?
    customer = lookup_customer(caller)
    if customer and customer.tier == "vip":
        return {
            "prompt":  f"You are a VIP concierge for {customer.name}. Be proactive…",
            "voice":   "john",
            "product": "storm-base",
        }
    return {}  # default agent handles non-VIPs
 
def lookup_customer(phone: str):
    # ... your CRM integration ...
    pass
Express
import crypto from "node:crypto";
import express from "express";
 
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
 
function verify(body, sig) {
  const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
  return sig &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
 
app.post(
  "/thunderphone/incoming",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
 
    const IMPORTANT_TYPES = new Set([
      "telephony.incoming",
      "web.incoming",
    ]);
    if (!IMPORTANT_TYPES.has(event.type)) return res.json({});
 
    const customer = await lookupCustomer(event.data.from_number);
    if (customer?.tier === "vip") {
      return res.json({
        prompt:  `You are a VIP concierge for ${customer.name}. Be proactive…`,
        voice:   "john",
        product: "storm-base",
      });
    }
    res.json({}); // fall back to default agent
  },
);

3. סכמת תגובה

גוף התגובה תואם בדיוק את סכמת התגובה לשיחה נכנסת. השדות הנפוצים:

שדהסוגתיאור
promptמחרוזת (חובה)הנחיית מערכת עבור הסוכן
voiceמחרוזת (חובה)מזהה קול מתוך GET /v1/voices
productמחרוזתברירת המחדל היא spark. מוצרים ניסיוניים זמינים רק לארגונים ברשימת ההרשאות.
background_trackמחרוזת | nullמזהה שמע סביבתי
acknowledgement_prompt_modeמחרוזתauto או manual (עבור Storm-with-ack בלבד)
acknowledgement_promptמחרוזתחובה כאשר המצב הוא manual
language_switchingמחרוזתעבור סוכנים רב-לשוניים, ask_first או follow_caller
toolsמערךסכמות מוטבעות של כלי פונקציות — ראו כלי פונקציות

שמרו סוכן והעבירו משתנים

החזירו {"agent_id": 12, "variables": {"name": "Ada"}} כדי להשתמש בסוכן השמור של אותו ארגון עם נתונים לכל שיחה. ההנחיה שלו יכולה להכיל {{name}} או {{name|Friend}}. משתני וובהוק גוברים על משתנים ברמת הבקשה; ערך null משתמש בברירת המחדל של מציין המיקום, או בטקסט ריק אם לא סופק ערך. הערכים הסופיים והשמות שלא נפתרו מופיעים בפרטי השיחה ובוובהוקים של השלמה. תגובות של סוכן שמור מקבלות רק את agent_id, variables ו-timezone. אם prompt קיים, התגובה משתמשת בתצורה מוטמעת ומתעלמת מ-agent_id (כולל מטא-נתונים מסוג null או שאינם מספרים שלמים); ההנחיה המוטמעת עדיין חייבת להיות תקפה. תגובות של תצורה מוטמעת יכולות לכלול גם variables. תגובות של סוכן שמור משתמשות בפיצול A/B שהופעל עבור הסוכן גם בשיחות טלפון וגם בשיחות וידג'ט, ולאחר מכן מעבדות את המשתנים. עיינו ב-משתני שיחה למידע על מגבלות ותמיכה ב-API של הפעלה. תצורת חסימה מגיעה מ-URL מדור קודם של מספר טלפון או ארגון, או ממפתח וידג'ט במצב וובהוק; אירועים נכנסים של מערכת נקודות הקצה הם התראות בלבד.

הגדירו את אזור הזמן לכל שיחה

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

{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}

שלחו את האזור במקום חותמת זמן ב-variables: השעון של הסוכן ממשיך לפעול במהלך השיחה, וזמן שתעבירו יתיישן. ערך שאינו אזור IANA מתעלמים ממנו, והשיחה מתחברת באזור של הסוכן.

דפוסים

הקשר של משתמש מחובר

בווידג'טים במצב וובהוק, דף המבקר כבר יודע מי הם המשתמשים. קראו לוובהוק שלכם עם פרמטר של מחרוזת שאילתה שערכת ה-SDK של הווידג'ט מעבירה הלאה (?customer_id=123), וחפשו את הלקוח בצד השרת.

השקת הנחיות מסוג A/B

לפני שתממשו זאת בעצמכם, שימו לב של-ThunderPhone יש יכולת מובנית של ניסויים (/dashboard/experiments ולשונית A/B בבונה הסוכנים), אשר מגדירה גרסאות, מפצלת תעבורה ומשווה תוצאות לכל גרסה — ללא צורך בוובהוק.

אם בכל זאת אתם זקוקים לשליטה בצד הוובהוק: בצעו גיבוב של call_id → דלי; הגישו הנחיה A עבור 0..49 והנחיה B עבור 50..99. תעדו בבסיס הנתונים שלכם את הדלי שבחרתם, ולאחר מכן קשרו אותו לציון של השיחה שהושלמה.

ניתוב מבוסס זמן

שעות פעילות → סוכן "תמיכה חיה"; מחוץ לשעות הפעילות → סוכן "קבלת הודעה". בצעו מיתוג פשוט על new Date().getUTCHours() במטפל שלכם.


השלבים הבאים