Open in
telephony.complete / web.complete
कॉल समाप्त होने पर ट्रांसक्रिप्ट, रिकॉर्डिंग URL और मेट्रिक्स के साथ डिलीवर किया जाने वाला नॉन-ब्लॉकिंग वेबहुक।
हर कॉल समाप्त होने के बाद एक कंप्लीशन इवेंट ट्रिगर होता है — इनबाउंड टेलीफोनी, आउटबाउंड टेलीफोनी, वेब कॉल या टेस्ट कॉल (बिल्डर माइक सेशन)। यह नॉन-ब्लॉकिंग है: किसी भी 2xx के साथ रिस्पॉन्ड करें।
इवेंट दोनों पाथ पर डिलीवर होता है:
- Webhook एंडपॉइंट्स को
telephony.complete(फोन कॉल) याweb.complete(वेब कॉल और बिल्डर माइक टेस्ट कॉल) मिलता है, जिसमें नीचे डॉक्यूमेंट किया गया स्थिर पेलोड, हर डिलीवरी के लिए एकevent_id, 30 s का टाइमआउट और 24 h तक रिट्राई शामिल हैं। - लीगेसी सिंगल-URL webhook को थोड़ा अलग पेलोड के साथ एक सिंक्रोनस प्रयास मिलता है (10 s टाइमआउट, कोई रिट्राई नहीं) — देखें लीगेसी पेलोड अंतर।
अनुरोध पेलोड (एंडपॉइंट डिलीवरी)
{
"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 | integer | इस कॉल के हर इवेंट में स्थिर रहता है |
agent_id | integer | null | कॉल संभालने वाला एजेंट, जब कोई असाइन किया गया हो |
agent_name | string | null | कॉल संभालने वाला एजेंट, जब कोई असाइन किया गया हो |
direction | string | inbound, outbound, web, test। ऐतिहासिक पेलोड में लेगसी mic या widget वैल्यू हो सकती हैं |
from_number, to_number | string | E.164। वेब कॉल और टेस्ट कॉल के लिए from_number का शाब्दिक मान "web" होता है |
origin_domain | string | केवल वेब/टेस्ट के लिए — वह पेज ऑरिजिन जिस पर विजेट होस्ट किया गया था (mic सेशन के लिए खाली) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | शुरू/अंत समय से प्राप्त |
status | string | completed या failed |
end_reason | string | नीचे दी गई टेबल देखें |
product, voice | string | कॉल के समय प्रभावी एजेंट कॉन्फ़िगरेशन |
variables | object | कॉल शुरू होने पर स्नैपशॉट किए गए इनपुट वेरिएबल |
unresolved_variables | array | कॉल कॉन्फ़िगरेशन द्वारा संदर्भित, लेकिन कॉल शुरू होने पर उपलब्ध नहीं कराए गए वेरिएबल नाम |
transfer_number | string | null | कॉल ट्रांसफ़र होने पर सेट होता है |
recording_url | string | null | समाप्त होने वाला साइन किया हुआ URL; तुरंत डाउनलोड करें। कोई रिकॉर्डिंग आर्टिफैक्ट उपलब्ध न होने पर null |
recording_stopped_at | timestamp | null | जब कॉल समाप्त होने से पहले रिकॉर्डिंग रोकी गई; इस समय के बाद की कोई भी सामग्री रिकॉर्डिंग में नहीं होती। रिकॉर्डिंग कॉल के अंत तक चलने या कॉल रिकॉर्ड न होने पर null |
recording_stopped_reason | string | null | रिकॉर्डिंग जल्दी क्यों रुकी। warm_transfer: यह वॉर्म ट्रांसफ़र द्वारा डेस्टिनेशन को कॉल करने से ठीक पहले रुकती है और ट्रांसफ़र विफल होने तथा कॉलर के एजेंट के पास लौटने पर भी बंद रहती है। अन्य वैल्यू को जानकारी के रूप में मानें; और जोड़ी जा सकती हैं। recording_stopped_at के null होने पर null |
billable_minutes | number | बिल किए गए मिनट, निकटतम चौथाई मिनट तक राउंड किए गए (15-सेकंड की वृद्धि, न्यूनतम 0.25)। सीधे वॉइसमेल पर जाने वाली कॉल भी यहां अपने वास्तविक मीटर किए गए मिनट रिपोर्ट करती हैं, लेकिन शुल्क प्लान रेट पर एक मिनट तक सीमित रहता है। |
billing_total_cents | integer | USD सेंट |
transcripts | array | प्रति-टर्न ट्रांसक्रिप्ट एंट्री; ट्रांसक्रिप्ट उपलब्ध न होने पर खाली हो सकती हैं |
summary | string | null | सादे भाषा में कॉल के बाद का सारांश (गद्य के दो से चार वाक्य, अधिकतम 2,000 कैरेक्टर, कॉल की भाषा में लिखा गया: एजेंट की प्राथमिक भाषा, जब तक कॉल ने उसे ओवरराइड न किया हो), जब एजेंट बिल्डर में एकत्र करने के लिए डेटा के अंतर्गत एजेंट के लिए कॉल सारांश जनरेट करें चालू हो (call_summary_enabled एजेंट पर) और जनरेशन सफल हो; अन्यथा null, जिसमें सारांश के लिए बहुत छोटी कॉल, बिना ट्रांसक्रिप्ट वाली कॉल और रिकॉर्डिंग बंद वाले एजेंट शामिल हैं। सारांश एक्सट्रैक्शन के समान पोस्ट-कॉल स्टेप में जनरेट होता है, इसलिए कॉल-पूर्ण वेबहुक भेजने से पहले एक्सट्रैक्ट किए गए डेटा की प्रतीक्षा करें सेटिंग (extraction_blocks_completion) तय करती है कि यह इवेंट इसकी प्रतीक्षा करता है या नहीं: प्रतीक्षा मोड में यह इवेंट भेजे जाने से पहले सारांश तैयार होता है (सारांश को स्टेप के 75-सेकंड बजट में अधिकतम 30 सेकंड मिलते हैं); जब इवेंट प्रतीक्षा नहीं करता (never, या except_transfers के अंतर्गत ट्रांसफ़र), तो यह फ़ील्ड null होती है और सारांश call.data_extracted में दोबारा नहीं भेजा जाता, इसलिए इसे बाद में कॉल ऑब्जेक्ट के summary से पढ़ें। डैशबोर्ड में कॉल पेज पर भी उपलब्ध |
extracted_data | object | 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 |
steps_trace | array | केवल उन कॉल पर जिनके एजेंट के पास स्टेप्स या हैंडऑफ़्स हों। कॉल इनके माध्यम से जिस क्रम में चली: हर एंट्री में t_ms (कॉल शुरू होने के बाद के मिलीसेकंड), agent (एजेंट की कुंजी, उसकी ID के साथ a), step (स्टेप ID, या बिना स्टेप्स वाले एजेंट को हैंडऑफ़ पर null) और via (start, model, condition, on_enter या handoff) होते हैं। इसमें केवल ID और समय होते हैं, इसलिए रिकॉर्डिंग बंद होने पर भी इसे रखा जाता है |
collected_values | object | केवल उन कॉल पर जिनके एजेंट के पास स्टेप्स या हैंडऑफ़्स हों। एजेंट ने अपने स्टेप्स में जो वैल्यू एकत्र कीं, वे कलेक्ट फ़ील्ड नाम से कुंजीबद्ध होती हैं, साथ ही प्रत्येक स्टेप के on_enter_tool परिणाम के लिए <tool>_result होता है। कॉल की सामग्री संरक्षित न होने पर छोड़ा जाता है (रिकॉर्डिंग बंद) |
समाप्ति के कारण
| मान | अर्थ |
|---|---|
user_hangup | रिमोट पार्टी ने पहले कॉल काट दिया |
ai_hangup | AI ने जानबूझकर कॉल समाप्त किया |
ai_transfer | AI ने कॉल ट्रांसफ़र किया; transfer_number सेट है |
ai_warm_transfer | AI ने वॉर्म (अटेंडेड) ट्रांसफ़र पूरा किया |
voicemail_hangup | वॉइसमेल का पता चला और आपके voicemail_action के अनुसार कॉल समाप्त किया गया |
max_duration | कॉल अधिकतम अवधि सीमा तक पहुंच गई: एजेंट की या कॉल की अपनी सीमा, या 6 घंटे की प्लेटफ़ॉर्म सीमा। समय सीमा चेतावनी के बाद AI या कॉलर द्वारा समाप्त की गई कॉल में ai_hangup या user_hangup बना रहता है |
superseded | सेशन को एक नए सेशन से बदल दिया गया |
inbound_config_webhook_failed | वेबहुक मोड में किसी नंबर पर इनबाउंड कॉल: कॉन्फ़िगरेशन वेबहुक ने उपयोग योग्य कॉन्फ़िगरेशन नहीं लौटाया, इसलिए एजेंट के जवाब देने से पहले कॉल समाप्त कर दिया गया। 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 | इंटीजर | कॉल शुरू होने से ऑफ़सेट, ms। ऑडियो टाइमिंग ज्ञात होने पर मौजूद |
ttfa_ms | इंटीजर | मापे जाने पर model टर्न के लिए पहले ऑडियो तक का समय |
audio_url, audio_urls | स्ट्रिंग / ऐरे | प्रति-टर्न रिकॉर्ड होने पर टर्न के ऑडियो के लिए समाप्त होने वाले साइन किए गए URLs |
पूरी तरह संरचित टर्न हिस्ट्री के लिए (इंटरप्शन मार्कर,
ऐक-प्रॉम्प्ट और रॉ पोज़िशन के साथ), उपयोग करें
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 टूल में खोलें, या उन्हें अपने इवैल्यूएशन मॉडल से चलाएं।
ट्रांसफर / फेल्योर पर किसी मानवीय टीममेट को ट्रिगर करें।