Open in
telephony.complete / web.complete
कॉल संपल्यावर ट्रान्स्क्रिप्ट, रेकॉर्डिंग URL आणि मेट्रिक्ससह वितरित होणारा नॉन-ब्लॉकिंग webhook.
प्रत्येक कॉल संपल्यानंतर पूर्णता इव्हेंट ट्रिगर होतो — इनबाउंड टेलिफोनी, आउटबाउंड टेलिफोनी, वेब कॉल किंवा चाचणी कॉल (बिल्डर माइक सत्र). तो नॉन-ब्लॉकिंग आहे: कोणत्याही 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",
"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",
"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 | पूर्णांक | USD सेंट |
transcripts | अॅरे | प्रत्येक टर्नसाठी ट्रान्सक्रिप्ट नोंदी; ट्रान्सक्रिप्ट उपलब्ध नसल्यास रिक्त असू शकते |
extracted_data | ऑब्जेक्ट | null | status, fields, evidence, verification, field_reasons, आणि schema_version असलेला संरचित एक्स्ट्रॅक्शन परिणाम. प्रत्येक non-null फील्डमध्ये अचूक संरचनात्मकरीत्या तपासलेला उद्धरण मजकूर (जास्तीत जास्त 1,000 वर्ण), तसेच त्याची स्पीकर भूमिका आणि टर्न इंडेक्स असतो; मॉडेलने परत केलेले जास्त लांबीचे उद्धरण कापण्याऐवजी नाकारले जातात. फील्ड null असल्यास एव्हिडन्स null असते. verified म्हणजे प्रत्येक उमेदवाराला नेमका एक वैध स्वतंत्र निर्णय मिळाला. unavailable मध्ये चुकीचा स्वरूपाचा किंवा अंशतः मिळालेला व्हेरिफायर आउटपुटही समाविष्ट असतो; वैध अंशतः निर्णय तरीही लागू होतात, तर एकही वैध निर्णय नसलेले उमेदवार null केले जातात. status हे completed, failed, exhausted, skipped, किंवा skipped_recording_disabled असते; एजंटकडे एक्स्ट्रॅक्शन फील्ड नसल्यास null असते |
समाप्तीची कारणे
| मूल्य | अर्थ |
|---|---|
user_hangup | रिमोट पक्षाने प्रथम कॉल ठेवला |
ai_hangup | AI ने जाणूनबुजून कॉल समाप्त केला |
ai_transfer | AI ने कॉल ट्रान्सफर केला; transfer_number सेट केलेला असतो |
ai_warm_transfer | AI ने वॉर्म (अटेंडेड) ट्रान्सफर पूर्ण केला |
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 |
व्यत्यय चिन्हे, अॅक्नॉलेजमेंट prompt आणि मूळ स्थानांसह पूर्णपणे संरचित फेरी इतिहासासाठी,
GET /v1/calls/{call_id}/history वापरा.
जुन्या पेलोडमधील फरक
जुन्या सिंगल-URL वेबहुक एन्व्हलपचे स्वरूप
{"type": "telephony.complete" | "web.complete", "data": {…}} असे असून त्यात
event_id नसते, आणि त्यातील data एंडपॉइंट पेलोडपेक्षा वेगळे असते:
जुन्या पूर्णता पेलोडमध्ये agent_id आणि agent_name देखील असतात.
- फेरी अॅरे
transcriptsऐवजीhistoryअंतर्गत असतो (वरच्या प्रमाणेच फेरी स्कीमा). - फील्ड संच हा कॉल संपल्यावरचा मूळ अहवाल असतो आणि त्यात वरील तक्त्यापलीकडील अतिरिक्त अंतर्गत फील्ड असू शकतात — अज्ञात फील्डना केवळ माहितीपर माना.
- वेब कॉल (
direction: "web") मध्येfrom_number/to_numberवगळलेले असतात आणिorigin_domainजोडलेले असते. - बिल्डर मायक्रोफोन चाचणी कॉल जुन्या मार्गावर
telephony.completeम्हणून नोंदवले जातात (एंडपॉइंट सिस्टम त्यांनाweb.completeवर मॅप करते). - ट्रान्सफर समन्वय: कॉल ट्रान्सफरमध्ये संपल्यावर, जुना वेबहुक समकालिकपणे कॉल केला जातो आणि हस्तांतरणाचे लक्ष्य तयार नसल्याचे सूचित करण्यासाठी तो
{"transfer_ready": false}असे उत्तर देऊ शकतो. इतर कोणतेही उत्तर (किंवा जुना वेबहुक नसल्यास) ट्रान्सफर पुढे जाऊ देते. यासाठी एंडपॉइंट वितरणांचा कधीही सल्ला घेतला जात नाही.
हँडलरचे उदाहरण
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 साधनामध्ये कॉल उघडा किंवा त्यांना तुमच्या स्वतःच्या मूल्यमापन मॉडेलमधून चालवा.
ट्रान्सफर / अयशस्वीतेवर मानवी टीम सदस्याला सूचना द्या.