ThunderPhone 2.0 est disponible.En libre-service, à partir de 2 ¢/min.Découvrir l’annonce

Webhooks

telephony.complete / web.complete

Webhook non bloquant envoyé à la fin d’un appel, avec transcription, URL de l’enregistrement et métriques.

Un événement de fin est déclenché après chaque appel — téléphonie entrante, téléphonie sortante, appel web ou appel de test (session micro du builder). Il est non bloquant : répondez avec n’importe quel code 2xx.

L’événement est envoyé sur les deux chemins :

Charge utile de requête (livraisons d’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",
    "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"
}
ChampTypeDescription
call_idintegerStable pour chaque événement de cet appel
agent_idinteger | nullAgent qui a traité l’appel, lorsqu’un agent était attribué
agent_namestring | nullAgent qui a traité l’appel, lorsqu’un agent était attribué
directionstringinbound, outbound, web, test. Les charges utiles historiques peuvent contenir les anciennes valeurs mic ou widget
from_number, to_numberstringE.164. from_number est littéralement "web" pour les appels web et les appels de test
origin_domainstringWeb/test uniquement — l’origine de la page qui hébergeait le widget (vide pour les sessions micro)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDérivé du début et de la fin
statusstringcompleted ou failed
end_reasonstringVoir le tableau ci-dessous
product, voicestringConfiguration de l’agent active au moment de l’appel
variablesobjectVariables d’entrée capturées au démarrage de l’appel
unresolved_variablesarrayNoms des variables référencées par la configuration de l’appel, mais non fournies au démarrage de l’appel
transfer_numberstring | nullDéfini lorsque l’appel a été transféré
recording_urlstring | nullURL signée expirante ; téléchargez rapidement. null lorsqu’aucun enregistrement n’est disponible
billable_minutesnumberMinutes facturées, arrondies au quart de minute le plus proche (incréments de 15 secondes, minimum de 0.25). Les appels dirigés directement vers la messagerie vocale indiquent toujours ici leurs minutes réellement comptabilisées, mais la facturation est plafonnée à une minute au tarif du forfait.
billing_total_centsintegerCentimes USD
transcriptsarrayEntrées de transcription par tour ; peut être vide lorsqu’une transcription n’est pas disponible
summarystring | nullRésumé post-appel en langage naturel (deux à quatre phrases, au maximum 2 000 caractères, rédigé dans la langue de l’appel : la langue principale de l’agent, sauf si l’appel l’a remplacée) lorsque l’agent a activé Générer un résumé d’appel sous Données à collecter dans le générateur d’agent (call_summary_enabled sur l’Agent) et que la génération a réussi ; null dans le cas contraire, y compris pour les appels trop courts pour être résumés, les appels sans transcription et les agents dont l’enregistrement est désactivé. Le résumé est généré à la même étape post-appel que l’extraction ; le paramètre Attendre les données extraites avant d’envoyer le webhook de fin d’appel (extraction_blocks_completion) détermine donc si cet événement l’attend : dans un mode d’attente, le résumé est prêt avant l’envoi de cet événement (le résumé lui-même dispose d’au maximum 30 secondes dans le budget de 75 secondes de l’étape) ; lorsque l’événement n’attend pas (never, ou un transfert sous except_transfers), ce champ est null et le résumé n’est pas renvoyé dans call.data_extracted, consultez-le donc ultérieurement via summary sur l’objet Call. Également disponible sur la page d’appel dans le dashboard
extracted_dataobject | nullRésultat d’extraction structuré avec status, fields, evidence, verification, field_reasons et schema_version. Chaque champ non nul contient la citation exacte vérifiée structurellement (au maximum 1 000 caractères), ainsi que le rôle du locuteur et l’index du tour ; les citations plus longues renvoyées par le modèle sont rejetées au lieu d’être tronquées. La preuve est null lorsque le champ est null. verified signifie que chaque candidat a reçu exactement un verdict indépendant valide. unavailable couvre également les sorties de vérificateur mal formées ou partielles ; les verdicts partiels valides sont toujours appliqués, tandis que les candidats sans verdict valide sont définis sur null. status est completed, failed, exhausted, skipped ou skipped_recording_disabled ; null lorsque l’agent n’avait aucun champ d’extraction

Raisons de fin

ValeurSignification
user_hangupLe correspondant a raccroché en premier
ai_hangupL’IA a délibérément mis fin à l’appel
ai_transferL’IA a transféré l’appel ; transfer_number est défini
ai_warm_transferL’IA a effectué un transfert assisté
voicemail_hangupUne messagerie vocale a été détectée et l’appel s’est terminé conformément à votre voicemail_action
max_durationL’appel a atteint la durée maximale autorisée
supersededLa session a été remplacée par une session plus récente
unknownLa raison de fin n’a pas pu être déterminée

Format de transcription

Chaque entrée de transcripts correspond à un tour de conversation. Les rôles sont user (parole de l’appelant), model (parole de l’agent et appels d’outils), tool (résultats d’outils) et system (événements d’appel tels que les changements de langue).

[
  {
    "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"] }
    }
  }
]
ChampTypeDescription
rolechaîneuser, model, tool ou system
content_typechaînetext/plain pour la parole ; application/json pour les appels d’outils, les résultats d’outils et les événements système
contentchaîne | objetTexte de parole ou objet structuré présenté ci-dessus. Appels d’outils : {"tool_call": name, "arguments": {…}}. Résultats d’outils : {"tool_name": name, "response": {…}}
start_ms, end_msentierDécalages depuis le début de l’appel, en ms. Présents lorsque le timing audio est connu
ttfa_msentierDélai avant le premier audio pour un tour model, lorsqu’il est mesuré
audio_url, audio_urlschaîne / tableauURL signées expirantes pour l’audio du tour, lorsqu’il est enregistré par tour

Pour l’historique complet et structuré des tours (avec les marqueurs d’interruption, les prompts d’acquiescement verbal et les positions brutes), utilisez GET /v1/calls/{call_id}/history.

Différences de charge utile héritée

L’enveloppe webhook héritée à URL unique est {"type": "telephony.complete" | "web.complete", "data": {…}} sans event_id, et ses data diffèrent de la charge utile de l’endpoint :

La charge utile de fin héritée contient également agent_id et agent_name.

  • Le tableau des tours se trouve sous history, et non sous transcripts (même schéma de tour que ci-dessus).
  • L’ensemble des champs correspond au rapport brut de fin d’appel et peut inclure des champs internes supplémentaires au-delà du tableau ci-dessus : traitez les champs inconnus comme des informations.
  • Les appels web (direction: "web") omettent from_number / to_number et ajoutent origin_domain.
  • Les appels de test du micro dans le Builder sont signalés comme telephony.complete sur le chemin hérité (le système de l’endpoint les mappe vers web.complete).
  • Coordination du transfert : lorsqu’un appel se termine par un transfert, le webhook hérité est appelé de manière synchrone et peut répondre {"transfer_ready": false} pour indiquer que la cible du transfert n’est pas prête. Toute autre réponse (ou l’absence de webhook hérité) permet au transfert de se poursuivre. Les livraisons via l’endpoint ne sont jamais consultées à cette fin.

Exemple de gestionnaire

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

Cas d'utilisation courants

Intégration CRM

Enregistrez la transcription et l'URL d'enregistrement de chaque appel avec vos dossiers clients.

Analytique

Envoyez les transcriptions vers un pipeline pour la modélisation de sujets, l'extraction de signaux CSAT ou le suivi du taux de transfert.

Contrôle qualité

Ouvrez les appels dans un outil d'assurance qualité pour une révision humaine, ou analysez-les avec votre propre modèle d'évaluation.

Notifications

Alertez un membre de l'équipe lors d'un transfert ou d'un échec.


Associés