Open in
telephony.complete / web.complete
خطاف ويب غير حاجب يُسلَّم عند انتهاء مكالمة، مع نص مفرغ ورابط التسجيل والمقاييس.
يتم تشغيل حدث الإكمال بعد انتهاء كل مكالمة — الاتصالات الهاتفية الواردة، والاتصالات الهاتفية الصادرة، أو مكالمة الويب، أو مكالمة اختبار (جلسة ميكروفون أداة الإنشاء). وهو غير حاجب: استجب بأي رمز 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_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 عند عدم توفر تسجيل |
billable_minutes | رقم | الدقائق المفوترة، مقربة إلى أقرب ربع دقيقة (بزيادات 15 ثانية، بحد أدنى 0.25). تظل المكالمات التي تنتقل مباشرة إلى البريد الصوتي تُبلغ عن دقائقها الفعلية المقاسة هنا، لكن تُحدَّد الرسوم بحد أقصى دقيقة واحدة وفق سعر الباقة. |
billing_total_cents | عدد صحيح | سنتات الدولار الأمريكي |
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 أيضًا مخرجات المدقق المشوهة أو الجزئية؛ تظل الأحكام الجزئية الصالحة مطبقة، بينما تُحوَّل إلى قيمة خالية المرشحات التي لا تملك حكمًا صالحًا واحدًا. تكون status إحدى القيم completed أو failed أو exhausted أو skipped أو skipped_recording_disabled؛ وتكون null عندما لا تكون لدى الوكيل حقول استخراج |
أسباب الإنهاء
| القيمة | المعنى |
|---|---|
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 فيه عن حمولة نقطة النهاية:
تحمل حمولة الإكمال القديمة أيضًا agent_id وagent_name.
- توجد مصفوفة الأدوار ضمن
history، وليسtranscripts(بنفس مخطط الأدوار المذكور أعلاه). - مجموعة الحقول هي تقرير نهاية المكالمة الأولي، وقد تتضمن حقولًا داخلية إضافية تتجاوز الجدول أعلاه — تعامل مع الحقول غير المعروفة على أنها معلوماتية.
- مكالمات الويب (
direction: "web") لا تتضمنfrom_number/to_numberوتضيفorigin_domain. - تُبلّغ مكالمات اختبار الميكروفون في أداة الإنشاء بصيغة
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 });
},
);حالات الاستخدام الشائعة
احفظ النص المفرغ لكل مكالمة ورابط التسجيل إلى جانب سجلات عملائك.
أرسل النصوص المفرغة تدفقياً إلى مسار عمل لنمذجة الموضوعات، واستخراج مؤشرات CSAT، أو مراقبة معدل التحويل.
افتح المكالمات في أداة لضمان الجودة لمراجعتها بشرياً، أو مررها عبر نموذج التقييم الخاص بك.
نبّه زميلاً بشرياً عند التحويل أو الفشل.