telephony.complete / web.complete
Webhook לא חוסם שנשלח כאשר שיחה מסתיימת, עם תמלול, כתובת URL של ההקלטה ומדדים.
אירוע השלמה מופעל לאחר סיום כל שיחה — טלפוניה נכנסת, טלפוניה יוצאת, שיחת אינטרנט או שיחת בדיקה (הפעלת מיקרופון בבונה). הוא אינו חוסם: השיבו עם כל קוד 2xx.
האירוע נמסר בשני הנתיבים:
- נקודות קצה של וובהוק מקבלות
telephony.complete(שיחות טלפון) אוweb.complete(שיחות אינטרנט ושיחות בדיקת מיקרופון בבונה) עם המטען היציב המתועד להלן,event_idלכל מסירה, זמן קצוב של 30 שניות, ו- ניסיונות חוזרים למשך עד 24 שעות. - הוובהוק הישן עם כתובת URL יחידה מקבל ניסיון סינכרוני אחד (זמן קצוב של 10 שניות, ללא ניסיונות חוזרים) עם מטען שונה מעט — ראו הבדלים במטען הישן.
מטען הבקשה (מסירות לנקודות קצה)
{
"data": {
"billable_minutes": 1.25,
"billing_total_cents": 8,
"call_id": 987654321,
"direction": "inbound",
"duration_seconds": 54,
"end_reason": "user_hangup",
"end_time": "2026-04-20T18:25:04.822Z",
"from_number": "+14155550199",
"product": "spark",
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| שדה | סוג | תיאור |
|---|---|---|
call_id | מספר שלם | יציב בכל אירוע עבור שיחה זו |
direction | מחרוזת | inbound, outbound, web, test. מטענים היסטוריים עשויים להכיל את הערכים הישנים mic או widget |
from_number, to_number | מחרוזת | E.164. הערך של from_number הוא המחרוזת המילולית "web" עבור שיחות אינטרנט ושיחות בדיקה |
origin_domain | מחרוזת | אינטרנט/בדיקה בלבד — מקור הדף שאירח את הרכיב (ריק עבור הפעלות מיקרופון) |
start_time, end_time | חותמת זמן | ISO 8601 UTC |
duration_seconds | מספר שלם | null | נגזר מזמני ההתחלה והסיום |
status | מחרוזת | completed או failed |
end_reason | מחרוזת | ראו את הטבלה להלן |
product, voice | מחרוזת | תצורת הסוכן שהייתה בתוקף בזמן השיחה |
transfer_number | מחרוזת | null | מוגדר כאשר השיחה הועברה |
recording_url | מחרוזת | null | כתובת URL חתומה עם תפוגה; הורידו בהקדם. null כאשר אין תוצר הקלטה זמין |
billable_minutes | מספר | דקות שחויבו, מעוגלות לרבע הדקה הקרוב ביותר (מרווחים של 15 שניות, מינימום 0.25). שיחות ישירות לתא קולי עדיין מדווחות כאן את דקות המדידה בפועל, אך החיוב מוגבל לדקה אחת לפי תעריף התוכנית. |
billing_total_cents | מספר שלם | סנטים של דולר אמריקאי |
transcripts | מערך | רשומות תמלול לכל תור; עשוי להיות ריק כאשר תמלול אינו זמין |
סיבות סיום
| ערך | משמעות |
|---|---|
user_hangup | הצד המרוחק ניתק ראשון |
ai_hangup | הבינה המלאכותית סיימה את השיחה במכוון |
ai_transfer | הבינה המלאכותית העבירה את השיחה; transfer_number מוגדר |
ai_warm_transfer | הבינה המלאכותית השלימה העברה חמה (מלווה) |
voicemail_hangup | זוהה תא קולי והשיחה הסתיימה בהתאם ל-voicemail_action שלכם |
max_duration | השיחה הגיעה למגבלת משך הזמן המרבי |
superseded | ההפעלה הוחלפה בהפעלה חדשה יותר |
unknown | לא ניתן היה לקבוע את סיבת הסיום |
פורמט תמלול
כל רשומה ב-transcripts היא תור שיחה אחד. התפקידים הם
user (דיבור המתקשר), model (דיבור הסוכן וגם קריאות כלים),
tool (תוצאות כלים) ו-system (אירועי שיחה כגון החלפות
שפה).
[
{
"role": "user",
"content_type": "text/plain",
"content": "Hi, I'm calling about my appointment.",
"start_ms": 1200,
"end_ms": 4100,
"audio_url": "https://storage.example.com/…"
},
{
"role": "model",
"content_type": "text/plain",
"content": "Sure, what date works best?",
"start_ms": 4200,
"end_ms": 6100
},
{
"role": "model",
"content_type": "application/json",
"content": {
"tool_call": "search_appointments",
"arguments": { "date": "2026-04-21" }
}
},
{
"role": "tool",
"content_type": "application/json",
"content": {
"tool_name": "search_appointments",
"response": { "available_slots": ["9:00 AM", "2:00 PM"] }
}
}
]| שדה | סוג | תיאור |
|---|---|---|
role | מחרוזת | user, model, tool או system |
content_type | מחרוזת | text/plain לדיבור; application/json לקריאות כלים, לתוצאות כלים ולאירועי מערכת |
content | מחרוזת | אובייקט | טקסט דיבור, או האובייקט המובנה שמוצג לעיל. קריאות כלים: {"tool_call": name, "arguments": {…}}. תוצאות כלים: {"tool_name": name, "response": {…}} |
start_ms, end_ms | מספר שלם | היסטים מתחילת השיחה, באלפיות שנייה. קיימים כאשר תזמון האודיו ידוע |
ttfa_ms | מספר שלם | הזמן עד לאודיו הראשון עבור תור model, כאשר הוא נמדד |
audio_url, audio_urls | מחרוזת / מערך | כתובות URL חתומות עם תוקף מוגבל לאודיו של התור, כאשר ההקלטה מתבצעת לכל תור בנפרד |
להיסטוריית התורים המובנית המלאה (עם סמני הפרעה,
הנחיות אישור ומיקומים גולמיים), השתמשו ב-
GET /v1/calls/{call_id}/history.
הבדלים במטען נתונים מדור קודם
מעטפת ה-webhook מדור קודם עם כתובת URL יחידה היא
{"type": "telephony.complete" | "web.complete", "data": {…}} ללא
event_id, וה-data שלה שונה ממטען הנתונים של נקודת הקצה:
- מערך התורים נמצא תחת
history, ולא תחתtranscripts(אותה סכמת תורים כמו לעיל). - קבוצת השדות היא דוח גולמי של סיום שיחה, ויכולה לכלול שדות פנימיים נוספים מעבר לטבלה לעיל — התייחסו לשדות לא מוכרים כמידע בלבד.
- שיחות ווב (
direction: "web") אינן כוללות אתfrom_number/to_numberומוסיפות אתorigin_domain. - שיחות בדיקת מיקרופון ב-Builder מדווחות כ-
telephony.completeבנתיב מדור קודם (מערכת נקודת הקצה ממפה אותן ל-web.complete). - תיאום העברה: כאשר שיחה מסתיימת בהעברה, ה-webhook
מדור קודם נקרא באופן סינכרוני ועשוי להשיב
{"transfer_ready": false}כדי לציין שיעד ההעברה אינו מוכן. כל תגובה אחרת (או היעדר webhook מדור קודם) מאפשרת להעברה להמשיך. מסירות דרך נקודת הקצה לעולם אינן נבדקות לצורך זה.
מטפל לדוגמה
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, signature: str) -> bool:
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
@app.post("/thunderphone-webhook")
async def webhook(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(status_code=401)
event = json.loads(body)
if event["type"] in ("telephony.complete", "web.complete"):
data = event["data"]
# Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
turns = data.get("transcripts") or data.get("history") or []
await persist_call_record(
call_id=data["call_id"],
turns=turns,
recording_url=data.get("recording_url"),
)
if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
await notify_team(data.get("transfer_number"), data["call_id"])
return {"ok": True}import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, signature) {
const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
return signature &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
app.post(
"/thunderphone-webhook",
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"));
if (["telephony.complete", "web.complete"].includes(event.type)) {
const data = event.data;
// Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
const turns = data.transcripts ?? data.history ?? [];
await persistCallRecord({ ...data, turns });
if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
await notifyTeam(data.transfer_number, data.call_id);
}
}
res.json({ ok: true });
},
);תרחישי שימוש נפוצים
שמרו את תמלול כל שיחה ואת כתובת ה-URL של ההקלטה לצד רשומות הלקוחות שלכם.
הזרימו תמלולים למערך לצורך מידול נושאים, חילוץ אותות CSAT או מעקב אחר שיעורי העברה.
פתחו שיחות בכלי QA לסקירה אנושית, או העבירו אותן דרך מודל ההערכה שלכם.
הפעילו התראה לנציג אנושי בעת העברה או כשל.