telephony.complete / web.complete
ہر کال ختم ہونے کے بعد ایک تکمیل کا ایونٹ فعال ہوتا ہے — اِن باؤنڈ ٹیلی فونی، آؤٹ باؤنڈ ٹیلی فونی، ویب کال، یا ٹیسٹ کال (بلڈر مائیک سیشن)۔ یہ غیر مسدود ہے: کسی بھی 2xx کے ساتھ جواب دیں۔
ایونٹ دونوں راستوں پر پہنچایا جاتا ہے:
- Webhook endpoints کو
telephony.complete(فون کالز) یاweb.complete(ویب کالز اور بلڈر مائیک ٹیسٹ کالز) موصول ہوتا ہے، جس میں ذیل میں دستاویزی مستحکم payload، ہر ڈیلیوری کے لیے ایکevent_id، 30 سیکنڈ کا ٹائم آؤٹ، اور 24 گھنٹے تک دوبارہ کوششیں شامل ہیں۔ - پرانے سنگل-URL webhook کو قدرے مختلف payload کے ساتھ ایک ہم وقت کوشش موصول ہوتی ہے (10 سیکنڈ کا ٹائم آؤٹ، کوئی دوبارہ کوشش نہیں) — دیکھیں پرانے payload کے فرق۔
درخواست payload (endpoint ڈیلیوریز)
{
"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 | integer | اس کال کے ہر ایونٹ میں مستحکم رہتا ہے |
direction | string | inbound، outbound، web، test۔ تاریخی payloads میں پرانی mic یا widget اقدار شامل ہو سکتی ہیں |
from_number, to_number | string | E.164۔ ویب کالز اور ٹیسٹ کالز کے لیے from_number لفظی "web" ہوتا ہے |
origin_domain | string | صرف ویب/ٹیسٹ — وہ صفحہ origin جس نے widget کو ہوسٹ کیا (مائیک سیشنز کے لیے خالی) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | آغاز/اختتام سے اخذ کردہ |
status | string | completed یا failed |
end_reason | string | ذیل کا جدول دیکھیں |
product, voice | string | کال کے وقت نافذ ایجنٹ config |
transfer_number | string | null | کال منتقل ہونے پر سیٹ ہوتا ہے |
recording_url | string | null | میعاد ختم ہونے والا دستخط شدہ URL؛ فوراً ڈاؤن لوڈ کریں۔ جب کوئی ریکارڈنگ artifact دستیاب نہ ہو تو null |
billable_minutes | number | بل کیے گئے منٹ، قریب ترین چوتھائی منٹ تک گول کیے گئے (15 سیکنڈ کے اضافے، کم از کم 0.25)۔ براہِ راست voicemail پر جانے والی کالز بھی یہاں اپنے اصل میٹر کیے گئے منٹ رپورٹ کرتی ہیں، مگر چارج پلان کی شرح پر ایک منٹ تک محدود ہوتا ہے۔ |
billing_total_cents | integer | امریکی ڈالر سینٹس |
transcripts | array | ہر ٹرن کے transcript اندراجات؛ transcript دستیاب نہ ہونے پر خالی ہو سکتا ہے |
اختتام کی وجوہات
| قدر | مطلب |
|---|---|
user_hangup | ریموٹ فریق نے پہلے کال ختم کی |
ai_hangup | AI نے جان بوجھ کر کال ختم کی |
ai_transfer | AI نے کال منتقل کی؛ transfer_number سیٹ ہے |
ai_warm_transfer | AI نے warm (attended) منتقلی مکمل کی |
voicemail_hangup | voicemail کا پتا چلا اور کال آپ کے 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 | string | user، model، tool، یا system |
content_type | string | گفتگو کے لیے text/plain؛ ٹول کالز، ٹول نتائج، اور سسٹم ایونٹس کے لیے application/json |
content | string | object | گفتگو کا متن، یا اوپر دکھایا گیا اسٹرکچرڈ آبجیکٹ۔ ٹول کالز: {"tool_call": name, "arguments": {…}}۔ ٹول نتائج: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | کال کے آغاز سے آف سیٹس، ms۔ آڈیو ٹائمنگ معلوم ہونے پر موجود ہوتے ہیں |
ttfa_ms | integer | ناپے جانے پر model ٹرن کے لیے پہلی آڈیو تک کا وقت |
audio_url, audio_urls | string / array | ٹرن کی آڈیو کے لیے میعاد ختم ہونے والے دستخط شدہ URLs، جب آڈیو ہر ٹرن کے حساب سے ریکارڈ کی گئی ہو |
مکمل اسٹرکچرڈ ٹرن ہسٹری کے لیے (جس میں مداخلت کے مارکرز،
ack-prompts، اور خام پوزیشنز شامل ہیں)،
GET /v1/calls/{call_id}/history استعمال کریں۔
پرانے payload کے فرق
پرانا سنگل-URL webhook envelope
{"type": "telephony.complete" | "web.complete", "data": {…}} ہے، جس میں
کوئی event_id نہیں ہوتا، اور اس کا data endpoint payload سے مختلف ہے:
- ٹرن array،
transcriptsکے بجائےhistoryکے تحت ہوتا ہے (اوپر والا ہی ٹرن schema)۔ - فیلڈ سیٹ خام end-of-call رپورٹ ہے اور اس میں اوپر دی گئی جدول سے اضافی اندرونی فیلڈز شامل ہو سکتے ہیں — نامعلوم فیلڈز کو معلوماتی سمجھیں۔
- ویب کالز (
direction: "web") میںfrom_number/to_numberشامل نہیں ہوتے اورorigin_domainشامل کیا جاتا ہے۔ - Builder مائیک ٹیسٹ کالز پرانے path میں
telephony.completeکے طور پر رپورٹ ہوتی ہیں (endpoint سسٹم انہیںweb.completeسے میپ کرتا ہے)۔ - ٹرانسفر کوآرڈینیشن: جب کال ٹرانسفر میں ختم ہوتی ہے، تو پرانے webhook کو synchronously کال کیا جاتا ہے اور ہینڈ آف ہدف کے تیار نہ ہونے کا اشارہ دینے کے لیے یہ
{"transfer_ready": false}واپس کر سکتا ہے۔ کوئی بھی دوسرا جواب (یا پرانا webhook نہ ہونا) ٹرانسفر کو جاری رہنے دیتا ہے۔ اس مقصد کے لیے endpoint deliveries سے کبھی مشورہ نہیں کیا جاتا۔
ہینڈلر کی مثال
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 ٹول میں کھولیں، یا انہیں اپنے تشخیصی ماڈل کے ذریعے چلائیں۔
ٹرانسفر یا ناکامی پر کسی انسانی ٹیم کے رکن کو متحرک کریں۔