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

Developer cookbook

Configuration dynamique par appel

Choisissez l’agent qui répond — ou réécrivez son prompt et ses paramètres — séparément pour chaque appel entrant, selon une logique personnalisée dans un webhook que vous contrôlez.

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 des utilisateurs connectés, tests A/B de prompt — passez en mode webhook et laissez votre serveur décider.

Fonctionnement

  1. Abonnez-vous à l’événement telephony.incoming (téléphone) ou web.incoming (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 de widget comportent des champs spécifiques au widget à la place des numéros — consultez le schéma de requête).
  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 repli. Valeur par défaut sûre.

1. Configurer la destination webhook

Pour les numéros de téléphone, abonnez votre endpoint à telephony.incoming :

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 la vérification de signature.

Pour les sessions de widget, créez une clé publiable en mode="webhook" avec l’URL de votre endpoint intégrée :

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 enverra une requête POST à cette URL au début de chaque session.

2. Implémentez le gestionnaire

Trois règles pratiques :

  • Vérifiez la signature à chaque requête (voir Vérifier les signatures de webhook). Ne sautez pas cette étape en dev : faites-le correctement une fois, puis réutilisez-le.
  • Répondez rapidement. Dix secondes est la limite stricte, et chaque seconde représente 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 un prompt dynamique, précalculez-le et mettez-le en cache.
  • Basculez proprement. Tout état inattendu doit renvoyer {} afin que l'agent attribué statiquement prenne en charge l'appel.
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, 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
Express
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
  },
);

3. Schéma de réponse

Le corps de la réponse correspond exactement au schéma de réponse d'appel entrant. Les champs les plus couramment utilisés :

ChampTypeDescription
promptchaîne (obligatoire)Prompt système pour l'agent
voicechaîne (obligatoire)Identifiant vocal de GET /v1/voices
productchaîneValeur par défaut : spark
background_trackchaîne | nullIdentifiant de l'audio d'ambiance
acknowledgement_prompt_modechaîneauto ou manual (Storm avec acquiescements verbaux uniquement)
acknowledgement_promptchaîneObligatoire lorsque le mode est manual
toolstableauSchémas d'outils de fonction en ligne — voir Outils de fonction

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), puis recherchez le client côté serveur.

Déploiement A/B de prompt

Avant de développer cela vous-même, notez que ThunderPhone propose une fonctionnalité native Expériences (/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 — sans webhook.

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 la note de l’appel terminé.

Routage selon l’heure

Heures ouvrées → agent de « support en direct » ; en dehors des heures ouvrées → agent de « prise de message ». Simple commutation sur new Date().getUTCHours() dans votre gestionnaire.


Étapes suivantes