telephony.complete / web.complete
ਹਰ ਕਾਲ ਖਤਮ ਹੋਣ ਤੋਂ ਬਾਅਦ ਇੱਕ ਕੰਪਲੀਸ਼ਨ ਇਵੈਂਟ ਚਲਦਾ ਹੈ — ਇਨਬਾਊਂਡ ਟੈਲੀਫੋਨੀ, ਆਊਟਬਾਊਂਡ ਟੈਲੀਫੋਨੀ, ਵੈੱਬ ਕਾਲ, ਜਾਂ ਟੈਸਟ ਕਾਲ (ਬਿਲਡਰ ਮਾਈਕ ਸੈਸ਼ਨ)। ਇਹ ਨਾਨ-ਬਲਾਕਿੰਗ ਹੈ: ਕਿਸੇ ਵੀ 2xx ਨਾਲ ਜਵਾਬ ਦਿਓ।
ਇਵੈਂਟ ਦੋਵੇਂ ਪਾਥਾਂ 'ਤੇ ਡਿਲੀਵਰ ਕੀਤਾ ਜਾਂਦਾ ਹੈ:
- ਵੇਬਹੁੱਕ ਐਂਡਪੌਇੰਟ ਨੂੰ
telephony.complete(ਫ਼ੋਨ ਕਾਲਾਂ) ਜਾਂweb.complete(ਵੈੱਬ ਕਾਲਾਂ ਅਤੇ ਬਿਲਡਰ ਮਾਈਕ ਟੈਸਟ ਕਾਲਾਂ) ਹੇਠਾਂ ਦਸਤਾਵੇਜ਼ਿਤ ਸਥਿਰ ਪੇਲੋਡ, ਹਰ ਡਿਲੀਵਰੀ ਲਈ ਇੱਕevent_id, 30 s ਟਾਈਮਆਊਟ, ਅਤੇ 24 h ਤੱਕ ਰੀਟ੍ਰਾਈਆਂ ਦੇ ਨਾਲ ਮਿਲਦਾ ਹੈ। - ਪੁਰਾਣੇ ਸਿੰਗਲ-URL ਵੇਬਹੁੱਕ ਨੂੰ ਥੋੜ੍ਹਾ ਵੱਖਰਾ ਪੇਲੋਡ ਦੇ ਨਾਲ ਇੱਕ ਸਿੰਕ੍ਰੋਨਸ ਕੋਸ਼ਿਸ਼ ਮਿਲਦੀ ਹੈ (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",
"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 | ਪੂਰਨ ਅੰਕ | ਇਸ ਕਾਲ ਦੇ ਹਰ ਇਵੈਂਟ ਲਈ ਸਥਿਰ ਰਹਿੰਦਾ ਹੈ |
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 | ਸਟ੍ਰਿੰਗ | ਕਾਲ ਦੇ ਸਮੇਂ ਲਾਗੂ ਏਜੰਟ ਕੌਂਫਿਗ |
transfer_number | ਸਟ੍ਰਿੰਗ | null | ਕਾਲ ਟ੍ਰਾਂਸਫ਼ਰ ਹੋਣ 'ਤੇ ਸੈੱਟ ਹੁੰਦਾ ਹੈ |
recording_url | ਸਟ੍ਰਿੰਗ | null | ਮਿਆਦ ਪੁੱਗਣ ਵਾਲਾ ਸਾਈਨ ਕੀਤਾ URL; ਤੁਰੰਤ ਡਾਊਨਲੋਡ ਕਰੋ। ਜਦੋਂ ਕੋਈ ਰਿਕਾਰਡਿੰਗ ਆਰਟੀਫੈਕਟ ਉਪਲਬਧ ਨਾ ਹੋਵੇ ਤਾਂ null |
billable_minutes | ਨੰਬਰ | ਬਿੱਲ ਕੀਤੇ ਮਿੰਟ, ਨਜ਼ਦੀਕੀ ਚੌਥਾਈ ਮਿੰਟ ਤੱਕ ਰਾਊਂਡ ਕੀਤੇ ਜਾਂਦੇ ਹਨ (15-ਸਕਿੰਟ ਦੇ ਵਾਧੇ, ਘੱਟੋ-ਘੱਟ 0.25)। ਸਿੱਧੇ ਵੌਇਸਮੇਲ ਵਾਲੀਆਂ ਕਾਲਾਂ ਇੱਥੇ ਆਪਣੇ ਅਸਲ ਮੀਟਰ ਕੀਤੇ ਮਿੰਟ ਹੀ ਦਰਸਾਉਂਦੀਆਂ ਹਨ, ਪਰ ਪਲੈਨ ਦਰ 'ਤੇ ਚਾਰਜ ਇੱਕ ਮਿੰਟ ਤੱਕ ਸੀਮਿਤ ਹੁੰਦਾ ਹੈ। |
billing_total_cents | ਪੂਰਨ ਅੰਕ | USD ਸੈਂਟ |
transcripts | ਐਰੇ | ਹਰ ਵਾਰੀ ਦੀ ਟ੍ਰਾਂਸਕ੍ਰਿਪਟ ਐਂਟਰੀਆਂ; ਟ੍ਰਾਂਸਕ੍ਰਿਪਟ ਉਪਲਬਧ ਨਾ ਹੋਣ 'ਤੇ ਖਾਲੀ ਹੋ ਸਕਦਾ ਹੈ |
ਖਤਮ ਹੋਣ ਦੇ ਕਾਰਨ
| ਮੁੱਲ | ਅਰਥ |
|---|---|
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 | ਸਟ੍ਰਿੰਗ / ਐਰੇ | ਵਾਰੀ ਦੀ ਆਡੀਓ ਲਈ ਮਿਆਦ-ਸੀਮਤ ਸਾਈਨ ਕੀਤੇ URLs, ਜਦੋਂ ਆਡੀਓ ਹਰ ਵਾਰੀ ਲਈ ਰਿਕਾਰਡ ਕੀਤੀ ਗਈ ਹੋਵੇ |
ਪੂਰੀ ਤਰ੍ਹਾਂ ਸਟ੍ਰਕਚਰਡ ਵਾਰੀ ਇਤਿਹਾਸ ਲਈ (ਇੰਟਰਪਸ਼ਨ ਮਾਰਕਰਾਂ,
ack-prompts ਅਤੇ ਰਾਅ ਸਥਿਤੀਆਂ ਸਮੇਤ), ਵਰਤੋ
GET /v1/calls/{call_id}/history।
ਪੁਰਾਣੇ ਪੇਲੋਡ ਦੇ ਅੰਤਰ
ਪੁਰਾਣਾ ਸਿੰਗਲ-URL webhook ਐਨਵਲਪ ਹੈ
{"type": "telephony.complete" | "web.complete", "data": {…}}, ਜਿਸ ਵਿੱਚ
ਕੋਈ event_id ਨਹੀਂ ਹੁੰਦਾ, ਅਤੇ ਇਸਦਾ data ਐਂਡਪੁਆਇੰਟ ਪੇਲੋਡ ਤੋਂ ਵੱਖਰਾ ਹੁੰਦਾ ਹੈ:
- ਵਾਰੀ ਐਰੇ
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 ਟੂਲ ਵਿੱਚ ਖੋਲ੍ਹੋ ਜਾਂ ਉਨ੍ਹਾਂ ਨੂੰ ਆਪਣੇ ਮੁਲਾਂਕਣ ਮਾਡਲ ਰਾਹੀਂ ਚਲਾਓ।
ਟ੍ਰਾਂਸਫਰ / ਅਸਫਲਤਾ ਹੋਣ 'ਤੇ ਕਿਸੇ ਮਨੁੱਖੀ ਟੀਮ ਮੈਂਬਰ ਨੂੰ ਸੂਚਿਤ ਕਰੋ।