telephony.complete / web.complete

Ένα συμβάν ολοκλήρωσης ενεργοποιείται μετά το τέλος κάθε κλήσης — εισερχόμενη τηλεφωνική κλήση, εξερχόμενη τηλεφωνική κλήση, διαδικτυακή κλήση ή δοκιμαστική κλήση (συνεδρία μικροφώνου του εργαλείου δημιουργίας). Είναι μη αποκλειστικό: απαντήστε με οποιονδήποτε κωδικό 2xx.

Το συμβάν παραδίδεται και στις δύο διαδρομές:

Ωφέλιμο φορτίο αιτήματος (παραδόσεις τελικών σημείων)

{
  "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_idintegerΣταθερό σε κάθε συμβάν για αυτήν την κλήση
directionstringinbound, outbound, web, test. Τα ιστορικά ωφέλιμα φορτία ενδέχεται να περιέχουν παλαιού τύπου τιμές mic ή widget
from_number, to_numberstringE.164. Το from_number είναι η κυριολεκτική τιμή "web" για διαδικτυακές και δοκιμαστικές κλήσεις
origin_domainstringΜόνο για web/test — η προέλευση της σελίδας που φιλοξένησε το widget (κενό για συνεδρίες μικροφώνου)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullΠροκύπτει από την ώρα έναρξης/λήξης
statusstringcompleted ή failed
end_reasonstringΔείτε τον παρακάτω πίνακα
product, voicestringΔιαμόρφωση πράκτορα που ίσχυε κατά την ώρα της κλήσης
transfer_numberstring | nullΟρίζεται όταν η κλήση μεταφέρθηκε
recording_urlstring | nullΥπογεγραμμένο URL με λήξη· κατεβάστε το άμεσα. null όταν δεν υπάρχει διαθέσιμο στοιχείο εγγραφής
billable_minutesnumberΧρεώσιμα λεπτά, στρογγυλοποιημένα στο πλησιέστερο τέταρτο του λεπτού (προσαυξήσεις 15 δευτερολέπτων, ελάχιστο 0,25). Οι κλήσεις που μεταβαίνουν απευθείας στον τηλεφωνητή εξακολουθούν να αναφέρουν εδώ τα πραγματικά μετρημένα λεπτά τους, αλλά η χρέωση περιορίζεται σε ένα λεπτό με την τιμή του προγράμματος.
billing_total_centsintegerΣεντ USD
transcriptsarrayΚαταχωρίσεις απομαγνητοφώνησης ανά γύρο· μπορεί να είναι κενές όταν δεν είναι διαθέσιμη απομαγνητοφώνηση

Αιτίες τερματισμού

ΤιμήΣημασία
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"] }
    }
  }
]
ΠεδίοΤύποςΠεριγραφή
rolestringuser, model, tool ή system
content_typestringtext/plain για ομιλία· application/json για κλήσεις εργαλείων, αποτελέσματα εργαλείων και συμβάντα συστήματος
contentstring | objectΚείμενο ομιλίας ή το δομημένο αντικείμενο που εμφανίζεται παραπάνω. Κλήσεις εργαλείων: {"tool_call": name, "arguments": {…}}. Αποτελέσματα εργαλείων: {"tool_name": name, "response": {…}}
start_ms, end_msintegerΜετατοπίσεις από την έναρξη της κλήσης, σε ms. Υπάρχουν όταν είναι γνωστός ο χρονισμός ήχου
ttfa_msintegerΧρόνος έως τον πρώτο ήχο για μία σειρά model, όταν μετράται
audio_url, audio_urlsstring / arrayΥπογεγραμμένα URL με λήξη για τον ήχο της σειράς, όταν καταγράφεται ανά σειρά

Για το πλήρως δομημένο ιστορικό σειρών (με δείκτες διακοπών, προτροπές επιβεβαίωσης και ακατέργαστες θέσεις), χρησιμοποιήστε GET /v1/calls/{call_id}/history.

Διαφορές παλαιού φορτίου

Το παλαιό περιτύλιγμα webhook με ένα URL είναι {"type": "telephony.complete" | "web.complete", "data": {…}} χωρίς event_id, και το data του διαφέρει από το φορτίο του 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 });
  },
);

Συνήθεις περιπτώσεις χρήσης

Ενσωμάτωση CRM

Αποθηκεύστε το απομαγνητοφωνημένο κείμενο και το URL εγγραφής κάθε κλήσης μαζί με τις εγγραφές πελατών σας.

Αναλύσεις

Διοχετεύστε απομαγνητοφωνημένα κείμενα σε μια ροή επεξεργασίας για μοντελοποίηση θεμάτων, εξαγωγή σημάτων CSAT ή παρακολούθηση ποσοστού μεταφορών.

Έλεγχος ποιότητας

Ανοίξτε κλήσεις σε ένα εργαλείο QA για ανθρώπινο έλεγχο ή εκτελέστε τες μέσω του δικού σας μοντέλου αξιολόγησης.

Ειδοποιήσεις

Ειδοποιήστε έναν ανθρώπινο συνεργάτη σε περίπτωση μεταφοράς / αποτυχίας.


Σχετικά