telephony.complete / web.complete

Užbaigimo įvykis suaktyvinamas pasibaigus kiekvienam skambučiui — įeinančiam telefono skambučiui, išeinančiam telefono skambučiui, žiniatinklio skambučiui ar bandomajam skambučiui (kūrimo priemonės mikrofono sesijai). Jis yra neblokuojantis: atsakykite bet kuriuo 2xx.

Įvykis pristatomas abiem būdais:

Užklausos duomenys (pristatymai į galinius taškus)

{
  "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"
}
LaukasTipasAprašymas
call_idintegerNekinta visuose šio skambučio įvykiuose
directionstringinbound, outbound, web, test. Istorinėse užklausose gali būti senstelėjusios mic arba widget reikšmės
from_number, to_numberstringE.164. Žiniatinklio skambučiams ir bandomiesiems skambučiams from_number yra tiesioginė reikšmė "web"
origin_domainstringTik žiniatinkliui / bandymams — puslapio, kuriame buvo valdiklis, šaltinis (mikrofono sesijoms tuščia)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullApskaičiuojama pagal pradžios ir pabaigos laiką
statusstringcompleted arba failed
end_reasonstringŽr. toliau pateiktą lentelę
product, voicestringSkambučio metu galiojusi agento konfigūracija
transfer_numberstring | nullNustatoma, kai skambutis buvo peradresuotas
recording_urlstring | nullBaigianti galioti pasirašyta URL; atsisiųskite nedelsdami. null, kai įrašo artefaktas nepasiekiamas
billable_minutesnumberApmokestinamos minutės, suapvalintos iki artimiausio ketvirčio minutės (15 sekundžių intervalai, mažiausiai 0.25). Skambučiai, nukreipti tiesiai į balso paštą, čia vis tiek nurodo faktines pagal naudojimą apskaičiuotas minutes, tačiau mokestis ribojamas iki vienos minutės pagal plano tarifą.
billing_total_centsintegerUSD centai
transcriptsarrayKiekvieno pokalbio ėjimo transkripto įrašai; gali būti tuščias, kai transkriptas nepasiekiamas

Pabaigos priežastys

ReikšmėReikšmė
user_hangupKita šalis pirma nutraukė skambutį
ai_hangupAgentas sąmoningai baigė skambutį
ai_transferAgentas peradresavo skambutį; nustatytas transfer_number
ai_warm_transferAgentas užbaigė šiltąjį (su dalyvavimu) peradresavimą
voicemail_hangupAptiktas balso paštas ir skambutis baigtas pagal jūsų voicemail_action
max_durationSkambutis pasiekė maksimalią trukmės ribą
supersededSesiją pakeitė naujesnė sesija
unknownNepavyko nustatyti pabaigos priežasties

Nuorašo formatas

Kiekvienas transcripts įrašas yra vienas pokalbio etapas. Vaidmenys yra user (skambinančiojo kalba), model (balso agento kalba ir įrankių iškvietimai), tool (įrankių rezultatai) ir system (skambučio įvykiai, pvz., kalbos perjungimai).

[
  {
    "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"] }
    }
  }
]
LaukasTipasAprašymas
roleeilutėuser, model, tool arba system
content_typeeilutėtext/plain kalbai; application/json įrankių iškvietimams, įrankių rezultatams ir sistemos įvykiams
contenteilutė | objektasKalbos tekstas arba pirmiau pateiktas struktūrizuotas objektas. Įrankių iškvietimai: {"tool_call": name, "arguments": {…}}. Įrankių rezultatai: {"tool_name": name, "response": {…}}
start_ms, end_mssveikasis skaičiusPoslinkiai nuo skambučio pradžios milisekundėmis. Pateikiami, kai žinomas garso laikas
ttfa_mssveikasis skaičiusLaikas iki pirmojo garso model etape, kai išmatuotas
audio_url, audio_urlseilutė / masyvasBaigiančio galioti pasirašyti etapo garso URL, kai garsas įrašomas kiekvienam etapui

Jei reikia visos struktūrizuotos etapų istorijos (su pertraukimo žymomis, patvirtinimo raginimais ir neapdorotomis pozicijomis), naudokite GET /v1/calls/{call_id}/history.

Senesnio paketo skirtumai

Senesnės versijos vieno URL žiniatinklio kabliuko paketo apvalkalas yra {"type": "telephony.complete" | "web.complete", "data": {…}} be event_id, o jo data skiriasi nuo galinio taško paketo:


Pavyzdinė apdorojimo funkcija

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 });
  },
);

Dažniausi naudojimo atvejai

CRM integracija

Išsaugokite kiekvieno skambučio nuorašą ir įrašo URL kartu su klientų įrašais.

Analitika

Siųskite nuorašus į duomenų srautą temų modeliavimui, CSAT signalų išgavimui arba peradresavimo dažnio stebėjimui.

Kokybės peržiūra

Atidarykite skambučius kokybės užtikrinimo įrankyje žmogaus peržiūrai arba apdorokite juos savo vertinimo modeliu.

Pranešimai

Peradresavimo arba nesėkmės atveju informuokite komandos narį.


Susiję