Το ThunderPhone 2.0 είναι εδώ.Ξεκινήστε μόνοι σας, από 2¢/λεπτό.Διαβάστε την ανακοίνωση

Webhooks

telephony.complete / web.complete

Webhook χωρίς αποκλεισμό που παραδίδεται όταν τερματίζεται μια κλήση, με απομαγνητοφώνηση, URL εγγραφής και μετρήσεις.

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

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

Φορτίο αιτήματος (παραδόσεις endpoint)

{
  "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ακέραιοςΣταθερό σε κάθε συμβάν για αυτή την κλήση
agent_idακέραιος | nullΟ πράκτορας που χειρίστηκε την κλήση, όταν είχε εκχωρηθεί κάποιος
agent_nameσυμβολοσειρά | nullΟ πράκτορας που χειρίστηκε την κλήση, όταν είχε εκχωρηθεί κάποιος
directionσυμβολοσειράinbound, outbound, web, test. Τα ιστορικά φορτία μπορεί να περιέχουν τις παλαιότερες τιμές mic ή widget
from_number, to_numberσυμβολοσειράE.164. Το from_number είναι κυριολεκτικά "web" για κλήσεις ιστού και δοκιμαστικές κλήσεις
origin_domainσυμβολοσειράΜόνο για web/test — η προέλευση της σελίδας που φιλοξενούσε το widget (κενό για συνεδρίες mic)
start_time, end_timeχρονική σήμανσηISO 8601 UTC
duration_secondsακέραιος | nullΠροκύπτει από την ώρα έναρξης/λήξης
statusσυμβολοσειράcompleted ή failed
end_reasonσυμβολοσειράΔείτε τον παρακάτω πίνακα
product, voiceσυμβολοσειράΗ διαμόρφωση του πράκτορα που ίσχυε κατά τον χρόνο της κλήσης
variablesαντικείμενοΜεταβλητές εισόδου όπως καταγράφηκαν κατά την έναρξη της κλήσης
unresolved_variablesπίνακαςΟνόματα μεταβλητών που αναφέρονται στη διαμόρφωση της κλήσης αλλά δεν δόθηκαν κατά την έναρξη της κλήσης
transfer_numberσυμβολοσειρά | nullΟρίζεται όταν η κλήση μεταβιβάστηκε
recording_urlσυμβολοσειρά | nullΥπογεγραμμένο URL με λήξη· πραγματοποιήστε λήψη άμεσα. null όταν δεν υπάρχει διαθέσιμο στοιχείο εγγραφής
billable_minutesαριθμόςΧρεώσιμα λεπτά, στρογγυλοποιημένα στο πλησιέστερο τέταρτο του λεπτού (διαστήματα 15 δευτερολέπτων, ελάχιστο 0.25). Οι κλήσεις που μεταβαίνουν απευθείας στον τηλεφωνητή εξακολουθούν να αναφέρουν εδώ τα πραγματικά μετρημένα λεπτά τους, αλλά η χρέωση περιορίζεται σε ένα λεπτό με την τιμή του προγράμματος.
billing_total_centsακέραιοςΣεντ USD
transcriptsπίνακαςΚαταχωρίσεις απομαγνητοφώνησης ανά σειρά ομιλίας· μπορεί να είναι κενός όταν δεν είναι διαθέσιμη απομαγνητοφώνηση
extracted_dataαντικείμενο | nullΑποτέλεσμα δομημένης εξαγωγής με status, fields, evidence, verification, field_reasons και schema_version. Κάθε πεδίο που δεν είναι null περιλαμβάνει το ακριβές απόσπασμα που ελέγχθηκε δομικά (έως 1.000 χαρακτήρες), μαζί με τον ρόλο ομιλητή και τον δείκτη σειράς ομιλίας· μεγαλύτερα αποσπάσματα που επιστρέφονται από το μοντέλο απορρίπτονται αντί να περικόπτονται. Το evidence είναι 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 με λήξη για τον ήχο της εναλλαγής, όταν η εγγραφή γίνεται ανά εναλλαγή

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

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

Το παλαιό περιτύλιγμα webhook ενός URL είναι {"type": "telephony.complete" | "web.complete", "data": {…}} χωρίς event_id, και το data του διαφέρει από το ωφέλιμο φορτίο του τελικού σημείου:

Το παλαιό ωφέλιμο φορτίο ολοκλήρωσης περιλαμβάνει επίσης τα agent_id και agent_name.

  • Ο πίνακας εναλλαγών βρίσκεται στο history, όχι στο transcripts (το ίδιο σχήμα εναλλαγής όπως παραπάνω).
  • Το σύνολο πεδίων είναι η ακατέργαστη αναφορά τέλους κλήσης και μπορεί να περιλαμβάνει επιπλέον εσωτερικά πεδία πέρα από τον παραπάνω πίνακα — αντιμετωπίζετε τα άγνωστα πεδία ως πληροφοριακά.
  • Οι διαδικτυακές κλήσεις (direction: "web") παραλείπουν τα from_number / to_number και προσθέτουν το origin_domain.
  • Οι δοκιμαστικές κλήσεις μικροφώνου του Builder αναφέρονται ως telephony.complete στην παλαιά διαδρομή (το σύστημα τελικού σημείου τις αντιστοιχίζει σε web.complete).
  • Συντονισμός μεταφοράς: όταν μια κλήση ολοκληρώνεται με μεταφορά, το παλαιό webhook καλείται συγχρονισμένα και μπορεί να απαντήσει {"transfer_ready": false} για να υποδείξει ότι ο προορισμός μεταφοράς δεν είναι έτοιμος. Οποιαδήποτε άλλη απάντηση (ή η απουσία παλαιού webhook) επιτρέπει τη συνέχιση της μεταφοράς. Οι παραδόσεις τελικού σημείου δεν λαμβάνονται ποτέ υπόψη για αυτό.

Παράδειγμα χειριστή

Python (FastAPI)
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}
Node.js (Express)
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 για ανθρώπινο έλεγχο ή εκτελέστε τις μέσω του δικού σας μοντέλου αξιολόγησης.

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

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


Σχετικά