> ## 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.

# Configuration dynamique par appel

> Sélectionnez un agent — ou réécrivez un prompt — pour chaque appel entrant selon une logique personnalisée dans un webhook.

Par défaut, chaque numéro de téléphone et chaque clé publiable ont un agent statique
attribué. Lorsque vous avez besoin d'une personnalisation **par appelant** ou **par visiteur**
— routage VIP, contexte d'utilisateur connecté, tests A/B de prompt — passez en
mode webhook et laissez votre serveur décider.

## Fonctionnement

1. Abonnez-vous à l'événement [`telephony.incoming`](/fr/webhooks/events)
   (téléphone) ou [`web.incoming`](/fr/webhooks/events) (widget).
   Les deux sont des webhooks **bloquants** : ThunderPhone attend jusqu'à
   10 secondes votre réponse avant de poursuivre l'appel.
2. ThunderPhone vous envoie `{call_id, from_number, to_number}` (les sessions du widget
   contiennent des champs spécifiques au widget au lieu de numéros — consultez le
   [schéma de requête](/fr/webhooks/call-incoming)).
3. Votre serveur répond avec une configuration d'agent (prompt, voix,
   produit, outils). ThunderPhone utilise cette configuration pour l'appel.
4. Si vous renvoyez `{}`, expirez le délai ou rencontrez une erreur, l'agent
   attribué statiquement est utilisé comme solution de secours. Valeur par défaut sûre.

<Note>
  Fonctionne de manière identique pour les appels téléphoniques (`telephony.incoming`) et les
  sessions de widget (`web.incoming`), qu'ils soient transmis à un endpoint webhook
  ou au webhook monourl hérité.
</Note>

## 1. Configurer la destination du webhook

<Tabs>
  <Tab title="Appels téléphoniques">
    Pour les numéros de téléphone, abonnez votre endpoint à `telephony.incoming` :

    ```bash theme={null}
    curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "label":  "Prod call-incoming",
        "url":    "https://example.com/thunderphone/incoming",
        "events": ["telephony.incoming"]
      }'
    ```

    La réponse inclut un `secret` à usage unique — enregistrez-le ; vous l'utiliserez
    pour vérifier la signature.
  </Tab>

  <Tab title="Widget web">
    Pour les sessions de widget, créez une clé publiable en `mode="webhook"`
    avec l'URL de votre endpoint intégrée :

    ```bash theme={null}
    curl -X POST https://api.thunderphone.com/v1/publishable-key \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name":            "Dynamic widget",
        "mode":            "webhook",
        "webhook_url":     "https://example.com/thunderphone/widget-incoming",
        "allowed_domains": ["example.com"]
      }'
    ```

    Le widget effectuera une requête POST vers cette URL à chaque début de session.
  </Tab>
</Tabs>

## 2. Implémenter le gestionnaire

Trois règles empiriques :

* **Vérifiez la signature** pour chaque requête (voir
  [Vérifier les signatures de webhook](/fr/guides/verify-webhook-signatures)).
  Ne sautez pas cette étape en développement : faites-le correctement une fois, puis réutilisez.
* **Répondez rapidement**. Dix secondes est la limite stricte, et chaque seconde est
  du silence pour l'appelant. Effectuez des recherches en base de données si nécessaire, mais
  n'appelez pas de LLM en aval de manière synchrone : si vous souhaitez générer des
  prompts dynamiques, précalculez-les et mettez-les en cache.
* **Prévoyez un repli propre**. Tout état inattendu doit renvoyer `{}` afin que
  l'agent attribué statiquement prenne en charge l'appel.

<CodeGroup>
  ```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, sig: str) -> bool:
      expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, sig or "")

  @app.post("/thunderphone/incoming")
  async def incoming(request: Request):
      body = await request.body()
      if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
          raise HTTPException(401)

      event = json.loads(body)
      if event["type"] not in ("telephony.incoming", "web.incoming"):
          return {}  # fall back to default

      caller = event["data"]["from_number"]
      # Cheap DB lookup: is this a known VIP?
      customer = lookup_customer(caller)
      if customer and customer.tier == "vip":
          return {
              "prompt":  f"You are a VIP concierge for {customer.name}. Be proactive…",
              "voice":   "john",
              "product": "storm-base",
          }
      return {}  # default agent handles non-VIPs

  def lookup_customer(phone: str):
      # ... your CRM integration ...
      pass
  ```

  ```javascript 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, sig) {
    const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
    return sig &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  }

  app.post(
    "/thunderphone/incoming",
    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"));

      const IMPORTANT_TYPES = new Set([
        "telephony.incoming",
        "web.incoming",
      ]);
      if (!IMPORTANT_TYPES.has(event.type)) return res.json({});

      const customer = await lookupCustomer(event.data.from_number);
      if (customer?.tier === "vip") {
        return res.json({
          prompt:  `You are a VIP concierge for ${customer.name}. Be proactive…`,
          voice:   "john",
          product: "storm-base",
        });
      }
      res.json({}); // fall back to default agent
    },
  );
  ```
</CodeGroup>

## 3. Schéma de réponse

Le corps de la réponse correspond exactement au
[schéma de réponse des appels entrants](/fr/webhooks/call-incoming).
Les champs couramment utilisés :

| Champ                         | Type                 | Description                                                                           |
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------- |
| `prompt`                      | chaîne (obligatoire) | Prompt système de l'agent                                                             |
| `voice`                       | chaîne (obligatoire) | Identifiant de voix provenant de [`GET /v1/voices`](/api-reference/agents#voices)     |
| `product`                     | chaîne               | Utilise `spark` par défaut                                                            |
| `background_track`            | chaîne \| null       | Identifiant de l'audio d'ambiance                                                     |
| `acknowledgement_prompt_mode` | chaîne               | `auto` ou `manual` (Storm avec acquiescements verbaux uniquement)                     |
| `acknowledgement_prompt`      | chaîne               | Obligatoire lorsque le mode est `manual`                                              |
| `tools`                       | tableau              | Schémas d'outils de fonction intégrés — voir [Outils de fonction](/fr/tools/overview) |

<Note>
  Le speak-order par appel et `max_hold_seconds` ne sont pas disponibles dans la
  réponse du webhook. Configurez-les sur l'
  [Agent](/api-reference/agents) référencé.
</Note>

## Modèles

### Contexte de l’utilisateur connecté

Dans les widgets en mode webhook, la page du visiteur sait déjà qui il
est. Appelez votre webhook avec un paramètre de chaîne de requête que le SDK du
widget transmet (`?customer_id=123`) et recherchez le client côté serveur.

### Déploiement de prompt A/B

Avant de développer cela vous-même, notez que ThunderPhone dispose d’une fonctionnalité native
[Expériences](/fr/guides/concepts)
(`/dashboard/experiments` et l’onglet **A/B** du générateur d’agents) qui
définit des variantes, répartit le trafic et compare les résultats par variante —
aucun webhook requis.

Si vous avez tout de même besoin d’un contrôle côté webhook : hachez `call_id` → compartiment ;
servez le prompt A pour `0..49` et le prompt B pour `50..99`. Enregistrez le
compartiment choisi dans votre propre base de données, puis corrélez-le ultérieurement avec l’évaluation
de l’appel terminé.

### Routage selon l’heure

Heures d’ouverture → agent « assistance en direct » ; hors horaires → agent « prise de message ».
Simple bascule sur `new Date().getUTCHours()` dans votre gestionnaire.

***

## Étapes suivantes

<CardGroup cols={2}>
  <Card title="Référence du webhook d’appel entrant" icon="phone" href="/fr/webhooks/call-incoming">
    Schémas exacts des requêtes et réponses, y compris chaque clé de configuration.
  </Card>

  <Card title="Vérifier les signatures de webhook" icon="shield-check" href="/fr/guides/verify-webhook-signatures">
    Configurez correctement le HMAC une fois ; réutilisez-le partout.
  </Card>

  <Card title="Créer une intégration d’outil" icon="screwdriver-wrench" href="/fr/guides/build-tool-integration">
    Combinez le routage dynamique avec des outils par agent.
  </Card>

  <Card title="Sémantique de livraison" icon="bolt" href="/fr/webhooks/overview">
    Tentatives, ordre, délais d’attente.
  </Card>
</CardGroup>
