ThunderPhone 2.0 è arrivato.Parti in autonomia, da 2¢/min.Leggi l’annuncio

Webhooks

telephony.complete / web.complete

Webhook non bloccante inviato al termine di una chiamata, con trascrizione, URL della registrazione e metriche.

Un evento di completamento viene attivato al termine di ogni chiamata: telefonia in entrata, telefonia in uscita, chiamata web o chiamata di test (sessione microfono del builder). È non bloccante: rispondi con qualsiasi codice 2xx.

L'evento viene recapitato tramite entrambi i percorsi:

Payload della richiesta (recapiti agli 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",
    "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"
}
CampoTipoDescrizione
call_idintegerStabile in tutti gli eventi di questa chiamata
directionstringinbound, outbound, web, test. I payload storici possono contenere i valori legacy mic o widget
from_number, to_numberstringE.164. from_number è il valore letterale "web" per le chiamate web e di test
origin_domainstringSolo web/test — l'origine della pagina che ospitava il widget (vuoto per le sessioni microfono)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDerivato dall'orario di inizio/fine
statusstringcompleted o failed
end_reasonstringConsulta la tabella seguente
product, voicestringConfigurazione dell'agente in uso al momento della chiamata
transfer_numberstring | nullImpostato quando la chiamata è stata trasferita
recording_urlstring | nullURL firmato con scadenza; scaricalo tempestivamente. null quando non è disponibile alcun file di registrazione
billable_minutesnumberMinuti fatturati, arrotondati al quarto di minuto più vicino (incrementi di 15 secondi, minimo 0,25). Le chiamate inoltrate direttamente alla segreteria riportano comunque qui i minuti effettivamente misurati, ma l'addebito è limitato a un minuto alla tariffa del piano.
billing_total_centsintegerCentesimi USD
transcriptsarrayVoci della trascrizione per turno; può essere vuoto quando una trascrizione non è disponibile

Motivi di fine

ValoreSignificato
user_hangupLa parte remota ha riagganciato per prima
ai_hangupL'IA ha terminato deliberatamente la chiamata
ai_transferL'IA ha trasferito la chiamata; transfer_number è impostato
ai_warm_transferL'IA ha completato un trasferimento assistito
voicemail_hangupÈ stata rilevata la segreteria telefonica e la chiamata è terminata in base a voicemail_action
max_durationLa chiamata ha raggiunto il limite massimo di durata
supersededLa sessione è stata sostituita da una più recente
unknownNon è stato possibile determinare il motivo della fine

Formato della trascrizione

Ogni voce in transcripts corrisponde a un turno della conversazione. I ruoli sono user (voce del chiamante), model (voce dell'agente e chiamate agli strumenti), tool (risultati degli strumenti) e system (eventi della chiamata come i cambi di lingua).

[
  {
    "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"] }
    }
  }
]
CampoTipoDescrizione
rolestringuser, model, tool o system
content_typestringtext/plain per la voce; application/json per le chiamate agli strumenti, i risultati degli strumenti e gli eventi di sistema
contentstring | objectTesto vocale oppure l'oggetto strutturato mostrato sopra. Chiamate agli strumenti: {"tool_call": name, "arguments": {…}}. Risultati degli strumenti: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffset dall'inizio della chiamata, in ms. Presenti quando è noto il timing audio
ttfa_msintegerTempo al primo audio per un turno model, quando misurato
audio_url, audio_urlsstring / arrayURL firmati a scadenza per l'audio del turno, quando registrato per turno

Per la cronologia dei turni completamente strutturata (con indicatori di interruzione, ack-prompt e posizioni grezze), usa GET /v1/calls/{call_id}/history.

Differenze del payload legacy

L'envelope webhook legacy a URL singolo è {"type": "telephony.complete" | "web.complete", "data": {…}} senza event_id, e il relativo data differisce dal payload dell'endpoint:

  • L'array dei turni è in history, non in transcripts (stesso schema dei turni mostrato sopra).
  • L'insieme dei campi è il report grezzo di fine chiamata e può includere campi interni aggiuntivi oltre a quelli della tabella sopra: considera i campi sconosciuti come informativi.
  • Le chiamate web (direction: "web") omettono from_number / to_number e aggiungono origin_domain.
  • Le chiamate di test del microfono Builder vengono segnalate come telephony.complete nel percorso legacy (il sistema endpoint le mappa a web.complete).
  • Coordinamento del trasferimento: quando una chiamata termina con un trasferimento, il webhook legacy viene chiamato in modo sincrono e può rispondere {"transfer_ready": false} per segnalare che la destinazione del passaggio non è pronta. Qualsiasi altra risposta (o l'assenza di un webhook legacy) consente al trasferimento di procedere. Le consegne dell'endpoint non vengono mai consultate per questo.

Gestore di esempio

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

Casi d'uso comuni

Integrazione CRM

Salva la trascrizione e l'URL della registrazione di ogni chiamata insieme ai tuoi record cliente.

Analisi

Invia le trascrizioni a una pipeline per la modellazione degli argomenti, l'estrazione di segnali CSAT o il monitoraggio del tasso di trasferimento.

Revisione della qualità

Apri le chiamate in uno strumento QA per la revisione umana oppure eseguile con il tuo modello di valutazione.

Notifiche

Avvisa un membro del team umano in caso di trasferimento / errore.


Correlati