---
title: "telephony.complete / web.complete"
description: "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 :

* Les **[points de terminaison webhook](/fr/webhooks/endpoints)** reçoivent
  `telephony.complete` (appels téléphoniques) ou `web.complete` (appels web et
  appels de test au micro du builder) avec la charge utile stable documentée ci-dessous,
  un `event_id` par livraison, un délai d’expiration de 30 s et des
  [tentatives pendant jusqu’à 24 h](/fr/webhooks/overview).
* Le **[webhook historique à URL unique](/api-reference/organizations#legacy-single-url-webhook)**
  reçoit une tentative synchrone unique (délai d’expiration de 10 s, aucune nouvelle tentative) avec une
  charge utile légèrement différente — consultez
  [Différences de charge utile historique](#legacy-payload-differences).

## Charge utile de requête (livraisons d’endpoint)

```json
{
  "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"
}
```

| Champ | Type | Description |
|-------|------|-------------|
| `call_id` | integer | Stable pour chaque événement de cet appel |
| `agent_id` | integer \| null | Agent qui a traité l’appel, lorsqu’un agent était attribué |
| `agent_name` | string \| null | Agent qui a traité l’appel, lorsqu’un agent était attribué |
| `direction` | string | `inbound`, `outbound`, `web`, `test`. Les charges utiles historiques peuvent contenir les anciennes valeurs `mic` ou `widget` |
| `from_number`, `to_number` | string | E.164. `from_number` est littéralement `"web"` pour les appels web et les appels de test |
| `origin_domain` | string | **Web/test uniquement** — l’origine de la page qui hébergeait le widget (vide pour les sessions micro) |
| `start_time`, `end_time` | timestamp | ISO 8601 UTC |
| `duration_seconds` | integer \| null | Dérivé du début et de la fin |
| `status` | string | `completed` ou `failed` |
| `end_reason` | string | Voir le tableau ci-dessous |
| `product`, `voice` | string | Configuration de l’agent active au moment de l’appel |
| `variables` | object | Variables d’entrée capturées au démarrage de l’appel |
| `unresolved_variables` | array | Noms des variables référencées par la configuration de l’appel, mais non fournies au démarrage de l’appel |
| `transfer_number` | string \| null | Défini lorsque l’appel a été transféré |
| `recording_url` | string \| null | URL signée expirante ; téléchargez rapidement. `null` lorsqu’aucun enregistrement n’est disponible |
| `billable_minutes` | number | Minutes 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_cents` | integer | Centimes USD |
| `transcripts` | array | Entrées de transcription par tour ; peut être vide lorsqu’une transcription n’est pas disponible |
| `summary` | string \| null | Ré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](/api-reference/agents)) 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](/api-reference/calls#call-object). Également disponible sur la page d’appel dans le dashboard |
| `extracted_data` | object \| null | Ré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

| Valeur | Signification |
|-------|---------|
| `user_hangup` | Le correspondant a raccroché en premier |
| `ai_hangup` | L’IA a délibérément mis fin à l’appel |
| `ai_transfer` | L’IA a transféré l’appel ; `transfer_number` est défini |
| `ai_warm_transfer` | L’IA a effectué un transfert assisté |
| `voicemail_hangup` | Une messagerie vocale a été détectée et l’appel s’est terminé conformément à votre `voicemail_action` |
| `max_duration` | L’appel a atteint la durée maximale autorisée |
| `superseded` | La session a été remplacée par une session plus récente |
| `unknown` | La 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).

```json
[
  {
    "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"] }
    }
  }
]
```

| Champ | Type | Description |
|-------|------|-------------|
| `role` | chaîne | `user`, `model`, `tool` ou `system` |
| `content_type` | chaîne | `text/plain` pour la parole ; `application/json` pour les appels d’outils, les résultats d’outils et les événements système |
| `content` | chaîne \| objet | Texte 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_ms` | entier | Décalages depuis le début de l’appel, en ms. Présents lorsque le timing audio est connu |
| `ttfa_ms` | entier | Délai avant le premier audio pour un tour `model`, lorsqu’il est mesuré |
| `audio_url`, `audio_urls` | chaîne / tableau | URL 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`](/api-reference/calls#get-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

<CodeGroup>
```python 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}
```

```javascript 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 });
  },
);
```
</CodeGroup>

---

## Cas d'utilisation courants

<CardGroup cols={2}>
  <Card title="Intégration CRM" icon="database">
    Enregistrez la transcription et l'URL d'enregistrement de chaque appel avec vos
    dossiers clients.
  </Card>
  <Card title="Analytique" icon="chart-line">
    Envoyez les transcriptions vers un pipeline pour la modélisation de sujets, l'extraction
    de signaux CSAT ou le suivi du taux de transfert.
  </Card>
  <Card title="Contrôle qualité" icon="clipboard-check">
    Ouvrez les appels dans un outil d'assurance qualité pour une révision humaine, ou analysez-les avec votre
    propre modèle d'évaluation.
  </Card>
  <Card title="Notifications" icon="bell">
    Alertez un membre de l'équipe lors d'un transfert ou d'un échec.
  </Card>
</CardGroup>

---

## Associés

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/fr/webhooks/call-incoming">
    L'équivalent bloquant qui s'exécute au début de l'appel.
  </Card>
  <Card title="Catalogue des événements" icon="list" href="/fr/webhooks/events">
    Autres types d'événements auxquels vous pouvez vous abonner.
  </Card>
  <Card title="API d'historique des appels" icon="phone" href="/api-reference/calls">
    Les mêmes données accessibles via REST pour le remplissage ou la relecture.
  </Card>
</CardGroup>
