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é lorsqu’un appel se termine, avec transcription, URL d’enregistrement et métriques.

Un événement de complétion se déclenche après la fin de 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é par les deux canaux suivants :

Charge utile de la requête (livraisons aux points de terminaison)

{
  "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"
}
ChampTypeDescription
call_idintegerStable pour chaque événement de cet appel
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 | nullCalculée à partir du début et de la fin
statusstringcompleted ou failed
end_reasonstringConsultez le tableau ci-dessous
product, voicestringConfiguration de l’agent active au moment de l’appel
transfer_numberstring | nullDéfini lorsque l’appel a été transféré
recording_urlstring | nullURL signée expirante ; téléchargez-la rapidement. null lorsqu’aucun artefact d’enregistrement n’est disponible
billable_minutesnumberMinutes facturées, arrondies au quart de minute le plus proche (incréments de 15 secondes, minimum 0.25). Les appels allant directement sur messagerie vocale indiquent toujours ici leurs minutes réellement mesurées, mais le montant est plafonné à une minute au tarif du forfait.
billing_total_centsintegerCentimes USD
transcriptsarrayEntrées de transcription par tour ; peut être vide lorsqu’aucune transcription n’est disponible

Raisons de fin

ValeurSignification
user_hangupL’interlocuteur distant 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 supervisé
voicemail_hangupUne messagerie vocale a été détectée et l’appel a pris fin conformément à votre voicemail_action
max_durationL’appel a atteint la limite de durée maximale
supersededLa session a été remplacée par une version 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 et les positions brutes), utilisez GET /v1/calls/{call_id}/history.

Différences de payload héritées

L'enveloppe webhook héritée à URL unique est {"type": "telephony.complete" | "web.complete", "data": {…}} sans event_id, et son data diffère du payload de l'endpoint :

  • Le tableau des tours se trouve sous history, et non sous transcripts (même schéma de tour que ci-dessus).
  • L'ensemble de champs correspond au rapport brut de fin d'appel et peut inclure des champs internes supplémentaires au-delà du tableau ci-dessus — considérez les champs inconnus comme informatifs.
  • Les appels web (direction: "web") omettent from_number / to_number et ajoutent origin_domain.
  • Les appels de test du microphone dans le Builder sont signalés comme telephony.complete sur le chemin hérité (le système de l'endpoint les associe à 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 par 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 les données de vos clients.

Analytique

Transmettez les transcriptions à un pipeline pour la modélisation des 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 exécutez-les dans votre propre modèle d'évaluation.

Notifications

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


Associé