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
- Abonnez-vous à l’événement
telephony.incoming(téléphone) ouweb.incoming(widget). Les deux sont des webhooks bloquants : ThunderPhone attend jusqu’à 10 secondes votre réponse avant de poursuivre l’appel. - 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). - Votre serveur répond avec une configuration d’agent (prompt, voix, produit, outils). ThunderPhone utilise cette configuration pour l’appel.
- 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.
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 ...
passimport 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 :
| Champ | Type | Description |
|---|---|---|
prompt | chaîne (obligatoire) | Prompt système pour l'agent |
voice | chaîne (obligatoire) | Identifiant vocal de GET /v1/voices |
product | chaîne | Valeur par défaut : spark |
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 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
Schémas exacts des requêtes et réponses, y compris chaque clé de configuration.
Configurez correctement le HMAC une fois ; réutilisez-le partout.
Combinez le routage dynamique avec des outils par agent.
Nouvelles tentatives, ordre, délais d’expiration.