ThunderPhone 2.0 è arrivato.Attivazione self-service, 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 consegnato su entrambi i percorsi:

Payload della richiesta (consegne 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_stopped_at": null,
    "recording_stopped_reason": null,
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
    "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"
}
CampoTipoDescrizione
call_idintegerStabile in ogni evento relativo a questa chiamata
agent_idinteger | nullAgente che ha gestito la chiamata, quando assegnato
agent_namestring | nullAgente che ha gestito la chiamata, quando assegnato
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 mic)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDerivato dall'ora di inizio/fine
statusstringcompleted o failed
end_reasonstringVedi la tabella seguente
product, voicestringConfigurazione dell'agente in vigore al momento della chiamata
variablesobjectVariabili di input salvate all'avvio della chiamata
unresolved_variablesarrayNomi delle variabili a cui fa riferimento la configurazione della chiamata ma non fornite all'avvio della chiamata
transfer_numberstring | nullImpostato quando la chiamata è stata trasferita
recording_urlstring | nullURL firmato con scadenza; scaricalo tempestivamente. null quando non è disponibile alcun artefatto di registrazione
recording_stopped_attimestamp | nullQuando la registrazione è stata interrotta prima della fine della chiamata; nella registrazione non è presente nulla dopo questo momento. null quando la registrazione è proseguita fino alla fine della chiamata o la chiamata non è stata registrata
recording_stopped_reasonstring | nullPerché la registrazione è stata interrotta anticipatamente. warm_transfer: si interrompe immediatamente prima che un trasferimento assistito chiami la destinazione e rimane disattivata anche se il trasferimento fallisce e il chiamante torna all'agente. Considera gli altri valori come informativi; potrebbero esserne aggiunti altri. null quando recording_stopped_at è null
billable_minutesnumberMinuti fatturati, arrotondati al quarto di minuto più vicino (incrementi di 15 secondi, minimo 0,25). Le chiamate dirette alla segreteria telefonica 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
summarystring | nullRiepilogo post-chiamata in linguaggio semplice (da due a quattro frasi di testo, al massimo 2.000 caratteri, scritto nella lingua della chiamata: la lingua principale dell'agente, salvo diversa impostazione durante la chiamata) quando l'agente ha attivato Genera un riepilogo della chiamata in Dati da raccogliere nel builder dell'agente (call_summary_enabled nell'Agente) e la generazione è riuscita; null negli altri casi, incluse le chiamate troppo brevi da riepilogare, le chiamate senza trascrizione e gli agenti con la registrazione disattivata. Il riepilogo viene generato nello stesso passaggio post-chiamata dell'estrazione, quindi l'impostazione Attendi i dati estratti prima di inviare il webhook di fine chiamata (extraction_blocks_completion) determina se questo evento lo attende: in una modalità di attesa il riepilogo è pronto prima dell'invio di questo evento (il riepilogo stesso riceve al massimo 30 secondi all'interno del budget di 75 secondi del passaggio); quando l'evento non attende (never o un trasferimento con except_transfers), questo campo è null e il riepilogo non viene inviato nuovamente in call.data_extracted, quindi leggilo in seguito da summary nell'oggetto Call. Disponibile anche nella pagina della chiamata nella dashboard
extracted_dataobject | nullRisultato dell'estrazione strutturata con status, fields, evidence, verification, field_reasons e schema_version. Ogni campo non nullo contiene la citazione esatta verificata strutturalmente (al massimo 1.000 caratteri), oltre al ruolo dell'interlocutore e all'indice del turno; le citazioni più lunghe restituite dal modello vengono rifiutate anziché troncate. L'evidenza è null quando il campo è null. verified indica che ogni candidato ha ricevuto esattamente un verdetto indipendente valido. unavailable copre anche output del verificatore non validi o parziali; i verdetti parziali validi vengono comunque applicati, mentre i candidati senza un verdetto valido vengono impostati su null. status è completed, failed, exhausted, skipped o skipped_recording_disabled; null quando l'agente non aveva campi di estrazione
steps_tracearraySolo per le chiamate il cui agente ha passaggi o handoff. L'ordine in cui la chiamata li ha attraversati: ogni voce contiene t_ms (millisecondi dall'avvio della chiamata), agent (la chiave dell'agente, a più il relativo ID), step (l'ID del passaggio, oppure null in caso di handoff a un agente senza passaggi) e via (start, model, condition, on_enter o handoff). Contiene solo ID e orari, quindi viene mantenuto anche quando la registrazione è disattivata
collected_valuesobjectSolo per le chiamate il cui agente ha passaggi o handoff. I valori raccolti dall'agente nei suoi passaggi, indicizzati per nome del campo di raccolta, più <tool>_result per il risultato on_enter_tool di ciascun passaggio. Ometto quando il contenuto della chiamata non viene conservato (registrazione disattivata)

Motivi di conclusione

ValoreSignificato
user_hangupLa controparte ha riagganciato per prima
ai_hangupL'AI ha terminato deliberatamente la chiamata
ai_transferL'AI ha trasferito la chiamata; transfer_number è impostato
ai_warm_transferL'AI 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: quello dell'agente, quello specifico della chiamata o il limite della piattaforma di 6 ore. Una chiamata terminata dall'AI o dal chiamante dopo l'avviso del limite di tempo mantiene ai_hangup o user_hangup
supersededLa sessione è stata sostituita da una più recente
inbound_config_webhook_failedChiamata in entrata a un numero in modalità webhook: il webhook di configurazione non ha restituito una configurazione utilizzabile, quindi la chiamata è stata terminata prima che un agente rispondesse. status è failed e la chiamata non viene fatturata
unknownNon è stato possibile determinare il motivo della terminazione

Formato della trascrizione

Ogni voce in transcripts corrisponde a un turno conversazionale. I ruoli sono user (voce del chiamante), model (voce dell'agente e chiamate di strumenti), tool (risultati degli strumenti) e system (eventi della chiamata, ad esempio 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 chiamate di strumenti, risultati degli strumenti ed eventi di sistema
contentstring | objectTesto vocale oppure l'oggetto strutturato mostrato sopra. Chiamate di strumenti: {"tool_call": name, "arguments": {…}}. Risultati degli strumenti: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffset dall'inizio della chiamata, in ms. Presenti quando la temporizzazione audio è nota
ttfa_msintegerTempo fino al primo audio per un turno model, quando misurato
audio_url, audio_urlsstring / arrayURL firmati in scadenza per l'audio del turno, quando registrato per turno

Per la cronologia dei turni completamente strutturata (con indicatori di interruzione, prompt di conferma 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 i relativi data differiscono dal payload dell'endpoint:

Il payload di completamento legacy include inoltre agent_id e agent_name.

  • L'array dei turni si trova in history, non in transcripts (stesso schema dei turni mostrato sopra).
  • L'insieme di campi è il report grezzo di fine chiamata e può includere campi interni aggiuntivi oltre a quelli nella tabella precedente: 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 nel Builder vengono segnalate come telephony.complete nel percorso legacy (il sistema dell'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 webhook legacy) consente al trasferimento di procedere. Le consegne all'endpoint non vengono mai consultate per questo.

Esempio di handler

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 dati dei clienti.

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 elaborale con il tuo modello di valutazione.

Notifiche

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


Correlati