Open in
telephony.complete / web.complete
ট্রান্সক্রিপ্ট, রেকর্ডিং URL এবং মেট্রিকসহ কল শেষ হলে নন-ব্লকিং webhook পাঠানো হয়।
প্রতিটি কল শেষ হওয়ার পরে একটি completion ইভেন্ট ট্রিগার হয় — ইনবাউন্ড টেলিফোনি, আউটবাউন্ড টেলিফোনি, ওয়েব কল বা টেস্ট কল (বিল্ডার মাইক সেশন)। এটি non-blocking: যেকোনো 2xx দিয়ে প্রতিক্রিয়া দিন।
ইভেন্টটি উভয় পাথে পাঠানো হয়:
- Webhook এন্ডপয়েন্ট গ্রহণ করে
telephony.complete(ফোন কল) অথবাweb.complete(ওয়েব কল এবং বিল্ডার মাইক টেস্ট কল), নিচে নথিভুক্ত স্থিতিশীল payload সহ, প্রতি ডেলিভারির জন্য একটিevent_id, 30 s timeout, এবং সর্বোচ্চ 24 h পর্যন্ত পুনঃচেষ্টা। - লিগ্যাসি একক-URL webhook সামান্য ভিন্ন payload সহ একটি synchronous প্রচেষ্টা গ্রহণ করে (10 s timeout, কোনো পুনঃচেষ্টা নেই) — দেখুন লিগ্যাসি payload-এর পার্থক্য।
রিকোয়েস্ট পেলোড (এন্ডপয়েন্ট ডেলিভারি)
{
"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 | 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 | শুধু ওয়েব/টেস্টের জন্য — উইজেট হোস্ট করা পৃষ্ঠার উৎস (মাইক সেশনের জন্য খালি) |
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 |
billable_minutes | number | বিল করা মিনিট, নিকটতম এক-চতুর্থাংশ মিনিটে রাউন্ড করা হয় (15-সেকেন্ডের বৃদ্ধি, সর্বনিম্ন 0.25)। সরাসরি ভয়েসমেইলে যাওয়া কলও এখানে তাদের প্রকৃত মিটার করা মিনিট রিপোর্ট করে, তবে চার্জ প্ল্যান রেটে এক মিনিটে সীমাবদ্ধ থাকে। |
billing_total_cents | integer | USD সেন্ট |
transcripts | array | প্রতিটি টার্নের ট্রান্সক্রিপ্ট এন্ট্রি; ট্রান্সক্রিপ্ট অনুপলভ্য হলে খালি হতে পারে |
extracted_data | object | null | status, fields, evidence, verification, field_reasons, এবং schema_version-সহ কাঠামোবদ্ধ এক্সট্রাকশন ফলাফল। প্রতিটি নন-নাল ফিল্ডে সঠিকভাবে কাঠামোগত যাচাই করা উদ্ধৃতি থাকে (সর্বোচ্চ 1,000 অক্ষর), সঙ্গে তার বক্তার ভূমিকা ও টার্ন ইনডেক্স; মডেল-ফেরত দীর্ঘ উদ্ধৃতি ছোট করে দেওয়ার পরিবর্তে প্রত্যাখ্যান করা হয়। ফিল্ড null হলে প্রমাণ null হয়। verified অর্থ প্রতিটি প্রার্থী ঠিক একটি বৈধ স্বাধীন রায় পেয়েছে। unavailable বিকৃত বা আংশিক ভেরিফায়ার আউটপুটও অন্তর্ভুক্ত করে; বৈধ আংশিক রায় প্রযোজ্য থাকে, আর একটি বৈধ রায়বিহীন প্রার্থীকে নাল করা হয়। 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 | ইন্টিজার | কল শুরুর পর থেকে অফসেট, ms। অডিওর টাইমিং জানা থাকলে উপস্থিত থাকে |
ttfa_ms | ইন্টিজার | পরিমাপ করা হলে, একটি model পালার প্রথম অডিও পর্যন্ত সময় |
audio_url, audio_urls | স্ট্রিং / অ্যারে | প্রতি-পালায় রেকর্ড করা হলে, সেই পালার অডিওর মেয়াদোত্তীর্ণ স্বাক্ষরিত URL |
বাধাদান চিহ্ন, স্বীকৃতি prompt এবং কাঁচা অবস্থানসহ সম্পূর্ণ স্ট্রাকচার্ড পালার ইতিহাসের জন্য,
GET /v1/calls/{call_id}/history ব্যবহার করুন।
লিগ্যাসি পেলোডের পার্থক্য
লিগ্যাসি একক-URL webhook এনভেলপটি হলো
{"type": "telephony.complete" | "web.complete", "data": {…}}, যেখানে
কোনো event_id নেই, এবং এর data এন্ডপয়েন্ট পেলোড থেকে ভিন্ন:
লিগ্যাসি কমপ্লিশন পেলোডে agent_id এবং agent_name-ও থাকে।
- পালার অ্যারে
transcripts-এর পরিবর্তেhistory-এর অধীনে থাকে (উপরের একই পালা স্কিমা)। - ফিল্ড সেটটি কাঁচা কল-শেষ রিপোর্ট এবং এতে উপরের টেবিলের বাইরেও অতিরিক্ত অভ্যন্তরীণ ফিল্ড থাকতে পারে — অজানা ফিল্ডগুলোকে তথ্যগত হিসেবে বিবেচনা করুন।
- ওয়েব কলগুলো (
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 টুলে কল খুলুন অথবা আপনার নিজস্ব মূল্যায়ন মডেলের মাধ্যমে সেগুলো চালান।
ট্রান্সফার বা ব্যর্থতার ক্ষেত্রে একজন মানব টিম সদস্যকে ট্রিগার করুন।