Open in
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, un agent statique est attribué à chaque numéro de téléphone et clé publiable. 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
- Vous vous abonnez à l’événement
telephony.incoming(téléphone) ouweb.incoming(widget). Les deux sont des webhooks bloquants : ThunderPhone attend votre réponse avant de poursuivre l’appel, jusqu’à 13 secondes pour un appel téléphonique (une nouvelle tentative incluse) et 10 secondes pour une session widget. - ThunderPhone vous envoie
{call_id, from_number, to_number}(les sessions widget contiennent des champs spécifiques au widget au lieu de 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.
- Il n’existe aucun agent de secours. Si vous renvoyez
{}ou une configuration non valide, ou si la requête échoue toujours après sa nouvelle tentative, l’appel téléphonique est immédiatement terminé et enregistré avecend_reasoninbound_config_webhook_failed; une requête de session widget échoue.
1. Configurer la destination du webhook
Pour les numéros de téléphone, abonnez votre point de terminaison à 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 point de terminaison 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 vers cette URL à chaque démarrage de session.
2. Implémentez le gestionnaire
Trois règles de base :
- Vérifiez la signature à chaque requête (voir Vérifier les signatures de webhook). Ne sautez pas cette étape en développement : faites-le correctement une fois et réutilisez-la.
- Répondez rapidement. La première tentative d'un appel téléphonique est interrompue au bout de 10 secondes, et l'échange complet au bout de 13 ; chaque seconde est un silence pour l'appelant. Effectuez des recherches dans la base de données si nécessaire, mais n'appelez pas les LLM en aval de manière synchrone : pour générer un prompt dynamique, pré-calculez et mettez en cache.
- Prévoyez un repli propre. Pour tout état inattendu, répondez avec un
promptet unevoicepar défaut (ou unagent_idenregistré). Renvoyer{}ou un statut d'erreur met fin à 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 des appels entrants. Les champs les plus utilisés :
| Champ | Type | Description |
|---|---|---|
prompt | chaîne (obligatoire) | Prompt système de l’agent |
voice | chaîne (obligatoire) | ID de voix de GET /v1/voices |
product | chaîne | Utilise spark par défaut. Les produits expérimentaux sont disponibles uniquement pour les organisations autorisées. |
background_track | chaîne | null | ID de l’audio ambiant |
acknowledgement_prompt_mode | chaîne | auto ou manual (Storm avec acquiescements verbaux uniquement) |
acknowledgement_prompt | chaîne | Obligatoire lorsque le mode est manual |
language_switching | chaîne | Pour les agents multilingues, ask_first ou follow_caller |
tools | tableau | Schémas d’outils de fonction intégrés — voir Outils de fonction |
Conserver un agent enregistré et fournir des variables
Renvoyez {"agent_id": 12, "variables": {"name": "Ada"}} pour utiliser l'agent
enregistré de cette organisation avec des données propres à l'appel. Son prompt peut contenir {{name}} ou
{{name|Friend}}. Les variables de webhook fusionnent avec les variables au niveau de la requête ; la valeur null
utilise la valeur par défaut de l'espace réservé, ou un texte vide si aucune n'est fournie. Les valeurs finales et les noms non résolus apparaissent dans les détails de l'appel et les webhooks de fin d'appel.
Les réponses utilisant un agent enregistré acceptent uniquement agent_id, variables et timezone. Si prompt est
présent, la réponse utilise une configuration inline et ignore agent_id (y compris
les métadonnées nulles ou non entières) ; le prompt inline doit tout de même être valide. Les réponses avec configuration inline peuvent également inclure variables. Les réponses utilisant un agent enregistré appliquent la répartition A/B déployée de l'agent aux appels téléphoniques et aux appels depuis le widget, puis rendent les variables. Consultez les variables d'appel
pour connaître les limites et la prise en charge de l'API de session. La configuration bloquante provient de l'URL historique du numéro de téléphone/de l'organisation ou d'une clé de widget en mode webhook ; les événements entrants du système de points de terminaison sont uniquement des notifications.
Définir le fuseau horaire par appel
Un agent peut répondre à des contacts situés dans différents fuseaux horaires. Ajoutez timezone, un nom IANA, à une réponse d’agent enregistré ou intégrée, et l’agent indique la date et l’heure, puis propose des créneaux dans ce fuseau horaire pour cet appel, avant son propre fuseau horaire :
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Envoyez le fuseau plutôt qu’un horodatage dans variables : l’horloge de l’agent continue de fonctionner pendant l’appel, et une heure transmise devient obsolète. Une valeur qui n’est pas un fuseau IANA est ignorée et l’appel se connecte dans le fuseau horaire de l’agent.
Modèles
Contexte d'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 progressif de prompt A/B
Avant de le développer 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 les 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 → bucket ;
servez le prompt A pour 0..49 et le prompt B pour 50..99. Enregistrez le bucket
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 d'ouverture → agent « support en direct » ; hors horaires → agent « prise de
message ». Effectuez simplement une bascule 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, puis réutilisez-le partout.
Combinez le routage dynamique avec des outils par agent.
Nouvelles tentatives, ordre, délais d'expiration.