> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# telephony.complete / web.complete

> Webhook non bloquant envoyé à la fin d’un appel, avec la transcription, l’URL de l’enregistrement et les 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é par les deux canaux suivants :

* 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 avec le micro du builder), avec la payload stable documentée ci-dessous,
  un `event_id` par livraison, un délai d’expiration de 30 s et des
  [nouvelles tentatives pendant jusqu’à 24 h](/fr/webhooks/overview).
* Le **[webhook à URL unique hérité](/api-reference/organizations#legacy-single-url-webhook)**
  reçoit une tentative synchrone unique (délai d’expiration de 10 s, sans nouvelles tentatives) avec une
  payload légèrement différente — voir
  [Différences de payload héritée](#legacy-payload-differences).

## Payload de la requête (livraisons aux points de terminaison)

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

| Champ                      | Type            | Description                                                                                                                                                                                            |
| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `call_id`                  | integer         | Stable dans chaque événement associé à cet appel                                                                                                                                                       |
| `direction`                | string          | `inbound`, `outbound`, `web`, `test`. Les payloads 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é des heures de début et de 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                                                                                                                                                   |
| `transfer_number`          | string \| null  | Défini lorsque l’appel a été transféré                                                                                                                                                                 |
| `recording_url`            | string \| null  | URL signée expirante ; téléchargez-la rapidement. `null` pour les sessions sans enregistrement                                                                                                         |
| `billable_minutes`         | number          | Minutes facturées, arrondies au quart de minute le plus proche (incréments de 15 secondes, minimum 0.25). Les appels allant directement sur la messagerie vocale sont facturés à un tarif fixe de 1 ¢. |
| `billing_total_cents`      | integer         | Centimes USD                                                                                                                                                                                           |
| `transcripts`              | array           | Transcription par tour — voir la section suivante                                                                                                                                                      |

### Raisons de fin

| Valeur             | Signification                                                                            |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `user_hangup`      | L’interlocuteur distant 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é (avec consultation)                                 |
| `voicemail_hangup` | La messagerie vocale a été détectée et l’appel a pris fin selon votre `voicemail_action` |
| `max_duration`     | L’appel a atteint la limite de durée maximale                                            |
| `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 des transcriptions

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 theme={null}
[
  {
    "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 minutage audio est connu                                                                                     |
| `ttfa_ms`                 | entier           | Délai jusqu'au 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é tour 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`](/api-reference/calls#get-history).

## Différences du payload hérité

L'enveloppe webhook héritée à URL unique est
`{"type": "telephony.complete" | "web.complete", "data": {…}}` sans
**aucun `event_id`**, et ses `data` diffèrent 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 micro du builder sont signalés comme `telephony.complete` sur le
  chemin hérité (le système d'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 signaler 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 d'endpoint ne sont jamais consultées à cette fin.

***

## Gestionnaire d'exemple

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  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) theme={null}
  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 les
    dossiers de vos clients.
  </Card>

  <Card title="Analytique" icon="chart-line">
    Transmettez 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="Revue 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 coéquipier humain en cas de transfert ou d'échec.
  </Card>
</CardGroup>

***

## Associé

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/fr/webhooks/call-incoming">
    L'équivalent bloquant exécuté 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 sont accessibles via REST pour le rattrapage ou la relecture.
  </Card>
</CardGroup>
