Open in
telephony.complete / web.complete
Webhook לא חוסם שנשלח כאשר שיחה מסתיימת, עם תמליל, כתובת URL של ההקלטה ומדדים.
אירוע השלמה מופעל לאחר סיום כל שיחה — טלפוניה נכנסת, טלפוניה יוצאת, שיחת ווב או שיחת בדיקה (הפעלת מיקרופון בבונה). הוא אינו חוסם: השיבו עם כל קוד 2xx.
האירוע נמסר בשני המסלולים:
- נקודות קצה של Webhook מקבלות
telephony.complete(שיחות טלפון) אוweb.complete(שיחות ווב ושיחות בדיקת מיקרופון בבונה) עם המטען היציב המתועד להלן,event_idלכל מסירה, פסק זמן של 30 שניות, ו- ניסיונות חוזרים למשך עד 24 שעות. - ה-Webhook מדור קודם עם כתובת 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",
"extracted_data": {
"status": "completed",
"fields": {
"customer_name": "Alex Morgan",
"appointment_date": "2026-04-23"
},
"evidence": {
"customer_name": {
"quote": "My name is Alex Morgan",
"speaker_role": "caller",
"turn_index": 4
},
"appointment_date": {
"quote": "April 23 works for me",
"speaker_role": "caller",
"turn_index": 7
}
},
"verification": "verified",
"field_reasons": {},
"schema_version": "92850758e231a3c95a..."
},
"from_number": "+14155550199",
"product": "spark",
"recording_stopped_at": null,
"recording_stopped_reason": null,
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"unresolved_variables": ["campaign_owner"],
"variables": {"campaign_name": "Spring renewals"},
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| שדה | סוג | תיאור |
|---|---|---|
call_id | מספר שלם | יציב בכל אירוע עבור שיחה זו |
agent_id | מספר שלם | null | הסוכן שטיפל בשיחה, כאשר הוקצה סוכן |
agent_name | מחרוזת | null | הסוכן שטיפל בשיחה, כאשר הוקצה סוכן |
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 | מחרוזת | תצורת הסוכן שהייתה בתוקף בזמן השיחה |
variables | אובייקט | משתני קלט שתועדו כצילום מצב בתחילת השיחה |
unresolved_variables | מערך | שמות משתנים שאליהם מתייחסת תצורת השיחה, אך לא סופקו בתחילת השיחה |
transfer_number | מחרוזת | null | מוגדר כאשר השיחה הועברה |
recording_url | מחרוזת | null | כתובת URL חתומה שפג תוקפה; הורידו אותה בהקדם. null כאשר אין ארטיפקט הקלטה זמין |
recording_stopped_at | חותמת זמן | null | המועד שבו ההקלטה נעצרה לפני שהשיחה הסתיימה; שום דבר לאחר רגע זה אינו נכלל בהקלטה. null כאשר ההקלטה נמשכה עד סוף השיחה או כאשר השיחה לא הוקלטה |
recording_stopped_reason | מחרוזת | null | הסיבה לכך שההקלטה נעצרה מוקדם. warm_transfer: ההקלטה נעצרת ממש לפני שהעברה חמה מתקשרת ליעד, ונשארת כבויה גם אם ההעברה נכשלת והמתקשר חוזר לסוכן. התייחסו לערכים אחרים כמידע בלבד; ייתכן שיתווספו ערכים נוספים. null כאשר recording_stopped_at הוא null |
billable_minutes | מספר | דקות מחויבות, מעוגלות לרבע הדקה הקרוב ביותר (מרווחים של 15 שניות, מינימום 0.25). שיחות ישירות לתא קולי עדיין מדווחות כאן על הדקות הנמדדות בפועל, אך החיוב מוגבל לדקה אחת לפי תעריף התוכנית. |
billing_total_cents | מספר שלם | סנטים של USD |
transcripts | מערך | רשומות תמלול לכל תור; יכול להיות ריק כאשר תמלול אינו זמין |
summary | מחרוזת | null | סיכום לאחר שיחה בשפה פשוטה (שתיים עד ארבעה משפטי פרוזה, עד 2,000 תווים, הכתובים בשפת השיחה: השפה הראשית של הסוכן אלא אם השיחה החליפה אותה) כאשר האפשרות יצירת סיכום שיחה מופעלת אצל הסוכן תחת נתונים לאיסוף בבונה הסוכנים (call_summary_enabled בסוכן) והיצירה הצליחה; אחרת null, כולל בשיחות קצרות מדי לסיכום, שיחות ללא תמלול וסוכנים שההקלטה אצלם מושבתת. הסיכום נוצר באותו שלב לאחר השיחה שבו מתבצעת החילוץ, לכן ההגדרה המתנה לנתונים שחולצו לפני שליחת הוובהוק של סיום השיחה (extraction_blocks_completion) קובעת אם אירוע זה ממתין לו: במצב המתנה הסיכום מוכן לפני שליחת אירוע זה (לסיכום עצמו מוקצות לכל היותר 30 שניות מתוך תקציב של 75 שניות לשלב); כאשר האירוע אינו ממתין (never, או העברה במסגרת except_transfers) שדה זה הוא null והסיכום אינו נשלח מחדש ב-call.data_extracted, לכן קראו אותו מאוחר יותר מתוך summary באובייקט השיחה. זמין גם בדף השיחה בלוח הבקרה |
extracted_data | אובייקט | null | תוצאת חילוץ מובנית הכוללת status, fields, evidence, verification, field_reasons ו-schema_version. לכל שדה שאינו null יש ציטוט מדויק שנבדק מבנית (עד 1,000 תווים), וכן תפקיד הדובר ואינדקס התור שלו; ציטוטים ארוכים יותר שהוחזרו מהמודל נדחים במקום להיחתך. הראיה היא null כאשר השדה הוא null. verified מציין שכל מועמד קיבל בדיוק הכרעה עצמאית תקפה אחת. unavailable כולל גם פלט מאמת פגום או חלקי; הכרעות חלקיות תקפות עדיין חלות, בעוד מועמדים ללא הכרעה תקפה אחת מאופסים ל-null. status הוא completed, failed, exhausted, skipped או skipped_recording_disabled; null כאשר לסוכן לא היו שדות חילוץ |
steps_trace | מערך | רק בשיחות שהסוכן שלהן כולל שלבים או העברות. הסדר שבו השיחה עברה דרכם: לכל רשומה יש t_ms (מילישניות מאז תחילת השיחה), agent (מפתח הסוכן, a בתוספת המזהה שלו), step (מזהה השלב, או null בהעברה לסוכן ללא שלבים) ו-via (start, model, condition, on_enter או handoff). מכיל מזהים וזמנים בלבד, ולכן נשמר גם כאשר ההקלטה כבויה |
collected_values | אובייקט | רק בשיחות שהסוכן שלהן כולל שלבים או העברות. הערכים שהסוכן אסף בשלבים שלו, עם מפתח לפי שם שדה האיסוף, וכן <tool>_result עבור תוצאת on_enter_tool של כל שלב. מושמט כאשר תוכן השיחה אינו נשמר (ההקלטה מושבתת) |
סיבות סיום
| ערך | משמעות |
|---|---|
user_hangup | הצד המרוחק ניתק ראשון |
ai_hangup | הבינה המלאכותית סיימה את השיחה בכוונה |
ai_transfer | הבינה המלאכותית העבירה את השיחה; transfer_number מוגדר |
ai_warm_transfer | הבינה המלאכותית השלימה העברה חמה (מלווה) |
voicemail_hangup | זוהתה תא קולי והשיחה הסתיימה בהתאם ל-voicemail_action שלכם |
max_duration | השיחה הגיעה למגבלת משך הזמן המרבי: המגבלה של הסוכן, המגבלה של השיחה עצמה, או מגבלת הפלטפורמה של 6 שעות. שיחה שהבינה המלאכותית או המתקשר מסיימים לאחר אזהרת מגבלת הזמן שומרת על ai_hangup או user_hangup |
superseded | ההפעלה הוחלפה בהפעלה חדשה יותר |
inbound_config_webhook_failed | שיחה נכנסת למספר במצב webhook: ה-webhook של התצורה לא החזיר תצורה שניתן להשתמש בה, ולכן השיחה הסתיימה לפני שסוכן ענה. הערך של status הוא failed והשיחה אינה מחויבת |
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 שלה שונה ממטען הנתונים של נקודת הקצה:
מטען נתוני ההשלמה מדור קודם כולל גם את agent_id ואת agent_name.
- מערך התורים נמצא תחת
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 לסקירה אנושית, או העבירו אותן דרך מודל ההערכה שלכם.
הפעילו התראה לנציג אנושי בעת העברה או כשל.