telephony.complete / web.complete
Ένα συμβάν ολοκλήρωσης ενεργοποιείται μετά το τέλος κάθε κλήσης — εισερχόμενη τηλεφωνική κλήση, εξερχόμενη τηλεφωνική κλήση, διαδικτυακή κλήση ή δοκιμαστική κλήση (συνεδρία μικροφώνου του εργαλείου δημιουργίας). Είναι μη αποκλειστικό: απαντήστε με οποιονδήποτε κωδικό 2xx.
Το συμβάν παραδίδεται και στις δύο διαδρομές:
- Τελικά σημεία webhook λαμβάνουν
telephony.complete(τηλεφωνικές κλήσεις) ήweb.complete(διαδικτυακές κλήσεις και δοκιμαστικές κλήσεις μικροφώνου του εργαλείου δημιουργίας) με το σταθερό ωφέλιμο φορτίο που τεκμηριώνεται παρακάτω, έναevent_idανά παράδοση, χρονικό όριο 30 δευτ. και επαναλήψεις για έως 24 ώρες. - Το παλαιού τύπου webhook μίας διεύθυνσης 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",
"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. Τα ιστορικά ωφέλιμα φορτία ενδέχεται να περιέχουν παλαιού τύπου τιμές mic ή widget |
from_number, to_number | string | E.164. Το from_number είναι η κυριολεκτική τιμή "web" για διαδικτυακές και δοκιμαστικές κλήσεις |
origin_domain | string | Μόνο για web/test — η προέλευση της σελίδας που φιλοξένησε το widget (κενό για συνεδρίες μικροφώνου) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Προκύπτει από την ώρα έναρξης/λήξης |
status | string | completed ή failed |
end_reason | string | Δείτε τον παρακάτω πίνακα |
product, voice | string | Διαμόρφωση πράκτορα που ίσχυε κατά την ώρα της κλήσης |
transfer_number | string | null | Ορίζεται όταν η κλήση μεταφέρθηκε |
recording_url | string | null | Υπογεγραμμένο URL με λήξη· κατεβάστε το άμεσα. null όταν δεν υπάρχει διαθέσιμο στοιχείο εγγραφής |
billable_minutes | number | Χρεώσιμα λεπτά, στρογγυλοποιημένα στο πλησιέστερο τέταρτο του λεπτού (προσαυξήσεις 15 δευτερολέπτων, ελάχιστο 0,25). Οι κλήσεις που μεταβαίνουν απευθείας στον τηλεφωνητή εξακολουθούν να αναφέρουν εδώ τα πραγματικά μετρημένα λεπτά τους, αλλά η χρέωση περιορίζεται σε ένα λεπτό με την τιμή του προγράμματος. |
billing_total_cents | integer | Σεντ USD |
transcripts | array | Καταχωρίσεις απομαγνητοφώνησης ανά γύρο· μπορεί να είναι κενές όταν δεν είναι διαθέσιμη απομαγνητοφώνηση |
Αιτίες τερματισμού
| Τιμή | Σημασία |
|---|---|
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 | 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 | Υπογεγραμμένα URL με λήξη για τον ήχο της σειράς, όταν καταγράφεται ανά σειρά |
Για το πλήρως δομημένο ιστορικό σειρών (με δείκτες διακοπών,
προτροπές επιβεβαίωσης και ακατέργαστες θέσεις), χρησιμοποιήστε
GET /v1/calls/{call_id}/history.
Διαφορές παλαιού φορτίου
Το παλαιό περιτύλιγμα webhook με ένα URL είναι
{"type": "telephony.complete" | "web.complete", "data": {…}} χωρίς
event_id, και το data του διαφέρει από το φορτίο του endpoint:
- Ο πίνακας σειρών βρίσκεται στο
history, όχι στοtranscripts(ίδιο σχήμα σειράς με παραπάνω). - Το σύνολο πεδίων είναι η ακατέργαστη αναφορά τέλους κλήσης και μπορεί να περιλαμβάνει πρόσθετα εσωτερικά πεδία πέρα από τον παραπάνω πίνακα — αντιμετωπίζετε τα άγνωστα πεδία ως ενημερωτικά.
- Οι διαδικτυακές κλήσεις (
direction: "web") παραλείπουν ταfrom_number/to_numberκαι προσθέτουν τοorigin_domain. - Οι δοκιμαστικές κλήσεις μικροφώνου του Builder αναφέρονται ως
telephony.completeστην παλαιά διαδρομή (το σύστημα endpoint τις αντιστοιχίζει σεweb.complete). - Συντονισμός μεταφοράς: όταν μια κλήση ολοκληρώνεται με μεταφορά, το
παλαιό webhook καλείται συγχρονισμένα και μπορεί να απαντήσει
{"transfer_ready": false}για να υποδείξει ότι ο προορισμός παράδοσης δεν είναι έτοιμος. Οποιαδήποτε άλλη απάντηση (ή η απουσία παλαιού webhook) επιτρέπει τη συνέχιση της μεταφοράς. Οι παραδόσεις endpoint δεν λαμβάνονται ποτέ υπόψη για αυτό.
Παράδειγμα χειριστή
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 για ανθρώπινο έλεγχο ή εκτελέστε τες μέσω του δικού σας μοντέλου αξιολόγησης.
Ειδοποιήστε έναν ανθρώπινο συνεργάτη σε περίπτωση μεταφοράς / αποτυχίας.