Open in
telephony.incoming / web.incoming
Webhook bloquant qui façonne la configuration d’un appel entrant en temps réel.
Lorsqu’un appel téléphonique entrant atteint un numéro sans agent
attribué, ou qu’une session de widget web démarre sur une clé publiable en
mode="webhook", ThunderPhone envoie une requête bloquante
telephony.incoming / web.incoming à votre
URL de webhook héritée
et attend une réponse de configuration : jusqu’à 13 secondes pour un
appel téléphonique, avec une nouvelle tentative incluse, et jusqu’à 10 secondes pour une session de widget
(voir Délais d’expiration et nouvelles tentatives). Utilisez cet
échange pour choisir dynamiquement un prompt, une voix et des outils pour chaque appel —
consultez le guide de configuration dynamique des appels
pour le modèle de bout en bout.
Délais d’attente et nouvelles tentatives
Répondez rapidement : l’appelant attend en ligne pendant que vous décidez.
Pour un appel téléphonique (telephony.incoming) :
- La première tentative peut prendre jusqu’à 10 secondes.
- En cas de délai d’attente, d’impossibilité de se connecter, ou de réponse
5xx,408ou429, ThunderPhone effectue une nouvelle tentative. La nouvelle tentative reçoit ce qui reste d’un budget de 13 secondes pour l’ensemble de l’échange : environ 3 secondes après une première tentative ayant expiré, et presque la totalité des 13 secondes après un échec rapide. Aucune nouvelle tentative n’est effectuée s’il reste moins de 2 secondes. - Toute autre réponse
4xx, une réponse dépassant la limite de taille, ou une réponse2xxdont le corps ne contient pas une configuration valide est définitive et ne fait pas l’objet d’une nouvelle tentative. - Les deux tentatives envoient le même corps, le même en-tête
X-ThunderPhone-Signature, et le même en-têteX-ThunderPhone-Idempotency-Key(telephony-incoming-<call_id>), afin qu’un gestionnaire ayant des effets de bord puisse reconnaître la nouvelle tentative.
Il n’existe aucun agent de secours. Si aucune configuration utilisable n’arrive, l’appel est
terminé immédiatement plutôt que de laisser l’appelant sur une ligne silencieuse. Il apparaît dans l’historique des appels comme un appel échoué, non facturé, avec
end_reason inbound_config_webhook_failed, et l’événement habituel
telephony.complete est envoyé.
Une session de widget (web.incoming) dispose d’une seule tentative pouvant durer jusqu’à 10
secondes. En cas d’échec, la requête de session échoue avec 502 ou 422.
Corps de la requête
Pour les appels téléphoniques (telephony.incoming) :
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| Champ | Type | Description |
|---|---|---|
call_id | integer | ID de l'appel — stable sur tous les événements de cet appel |
from_number | string | Numéro de l'appelant au format E.164 |
to_number | string | Destination E.164 (l'un de vos numéros ThunderPhone) |
Pour les sessions de widget web (web.incoming), data identifie la
page d'intégration au lieu des numéros de téléphone :
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| Champ | Type | Description |
|---|---|---|
call_id | entier | ID de l’appel |
origin_domain | chaîne | L’origine de la page qui héberge le widget, telle qu’envoyée par le navigateur. Un client autre qu’un navigateur peut envoyer n’importe quelle valeur ; ne la considérez donc pas comme une preuve de l’origine de la requête |
publishable_key_prefix | chaîne | Premiers caractères de la clé publiable qui a ouvert la session |
language, primary_language | chaîne | Présents lorsque la session du widget a demandé une surcharge de langue |
voice | chaîne | Présent lorsque la session du widget a demandé une surcharge de voix |
website_context | chaîne | Présent lorsque le widget a transmis un contexte de page par session |
Les champs de surcharge (language, voice, website_context et toutes les
variables du navigateur) arrivent uniquement lorsque la clé publiable les autorise dans ses
surcharges du navigateur. Une requête de session contenant une surcharge non autorisée par la clé est rejetée avant l’appel de ce webhook.
Schéma de réponse
Renvoyez un objet JSON décrivant la configuration de l’agent pour cet appel.
prompt et voice sont obligatoires ; tous les autres champs sont facultatifs.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| Champ | Type | Obligatoire | Description |
|---|---|---|---|
prompt | chaîne | oui | Prompt système qui pilote l’agent |
voice | chaîne | oui | ID de voix provenant de GET /v1/voices, par ex. john. voice_name est accepté comme alias. Les voix inconnues échouent à la validation et rejettent l’appel |
product | chaîne | non | Valeur par défaut : spark. Valeurs autorisées : spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | chaîne | non | minimal, base (par défaut) ou extra. Remplacé pour les produits Storm : storm-extra* force extra, les autres storm-* forcent base |
audio_context_mode | chaîne | non | full (par défaut) ou reduced |
watchdog_enabled | booléen | non | Active la supervision pour cet appel. Valeur par défaut : false |
additional_audio_context | booléen | null | non | Inclut les derniers tours audio de l’appelant plutôt que seulement le tour le plus récent, ce qui améliore les corrections et la collecte de données comportant beaucoup d’orthographe ou de chiffres, avec un léger surcoût de latence et de coût. Activé par défaut pour les sessions entrantes et désactivé pour les appels téléphoniques sortants ; null conserve la valeur par défaut |
storm_feedback_mode | chaîne | non | none, acknowledgement (par défaut) ou tick |
language | chaîne | non | Raccourci pour primary_language |
primary_language | chaîne | non | Code de langue, normalisé (valeur par défaut : en). Les codes non résolubles rejettent l’appel |
has_additional_languages | booléen | non | Valeur par défaut : false |
additional_languages | tableau de chaînes | non | Langues supplémentaires vers lesquelles l’agent peut basculer |
native_voice_switching | booléen | non | Valeur par défaut : false. Lorsque l’appel passe à une autre langue, remplace la voix configurée par une voix native de cette langue (correspondant au genre) |
language_switching | chaîne | null | non | ask_first (par défaut) ou follow_caller ; null équivaut à ask_first. Définit comment un agent multilingue gère l’interlocuteur qui parle une autre de ses langues. ask_first bascule immédiatement lorsqu’il demande la langue ou prononce une phrase complète dans celle-ci, et demande s’il faut continuer dans cette langue après quelque chose de court, comme un « oui ». follow_caller bascule vers toute langue configurée qu’il parle, même un seul mot. S’applique uniquement lorsque additional_languages est défini |
background_track | chaîne | null | non | ID d’audio ambiant ou null |
acknowledgement_prompt_mode | chaîne | non | auto (par défaut) ou manual (produits Storm-with-ack) |
acknowledgement_prompt | chaîne | non | Utilisé lorsque acknowledgement_prompt_mode="manual" |
silence_interval_seconds | entier | null | non | 5–120. Secondes de silence de l’appelant avant une relance |
silence_max_checkins | entier | null | non | 1–10 |
silence_checkins_enabled | booléen | non | Valeur par défaut : true |
connect_tone_enabled | booléen | non | Valeur par défaut : false |
pronunciation_dicts | booléen | null | non | Valeur par défaut : false ; null équivaut à false. Lorsque la valeur est true, la synthèse vocale Cartesia de l’appel utilise le dictionnaire de prononciation ThunderPhone pour la langue de l’appel, lorsqu’il existe. Fonctionne pour les appels téléphoniques et les appels depuis le widget. S’applique uniquement aux propres voix Cartesia de ThunderPhone, et non aux voix utilisant votre propre clé API Cartesia. Non utilisé avec agent_id, auquel cas les paramètres de l’agent enregistré s’appliquent |
voicemail_action | chaîne | non | prompt (par défaut), hangup ou message |
voicemail_message | chaîne | non | Utilisé lorsque voicemail_action="message" |
agent_name | chaîne | non | Nom d’affichage transmis aux tableaux de bord et au widget |
org_name | chaîne | non | Nom d’affichage de l’organisation pour le persona de l’agent |
tools | tableau | non | Schémas d’outils de fonction intégrés (voir Outils de fonction) |
transfer_destinations | tableau d’objets | null | non | Jusqu’à 50 entrées {name, number} vers lesquelles cet appel peut être transféré, validées comme les numéros de transfert d’un agent. L’agent transfère par nom et uniquement vers ces entrées ; [] signifie aucun transfert. Omettez ce champ ou envoyez null pour laisser l’agent transférer vers un numéro de son choix |
timezone | chaîne | non | Fuseau horaire IANA pour cet appel, tel que America/Chicago. L’agent indique la date et l’heure dans ce fuseau, avant son propre fuseau horaire. Également autorisé à côté de agent_id. Les valeurs qui ne sont pas des fuseaux IANA sont ignorées. Voir Définir le fuseau horaire par appel |
call_id | entier | non | Écho facultatif de l’ID d’appel de la requête ; ignoré |
Comme prompt et voice sont obligatoires, renvoyer {} ou toute
réponse qui échoue à la validation rejette l’appel : un appel téléphonique est terminé
comme décrit dans Délais d’attente et nouvelles tentatives, et une
demande de session de widget échoue avec 422. Il n’existe pas de solution de repli
vers un agent statique sur ce chemin (un numéro ou une clé en mode webhook n’a aucun agent
attribué).
Limite de taille de réponse
Exemple de gestionnaire
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(WEBHOOK_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"] == "telephony.incoming":
caller = event["data"]["from_number"]
prompt = (
"Greet the caller as a San Francisco local…"
if caller.startswith("+1415")
else "You are a friendly customer support agent…"
)
return {
"prompt": prompt,
"voice": "john",
"product": "spark",
}
if event["type"] == "web.incoming":
return {
"prompt": "You are the website's helpful voice assistant…",
"voice": "john",
"product": "spark",
}
return {}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" }),
(req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "telephony.incoming" || event.type === "web.incoming") {
const caller = event.data.from_number || "web";
const prompt = caller.startsWith("+1415")
? "Greet the caller as a San Francisco local…"
: "You are a friendly customer support agent…";
return res.json({
prompt,
voice: "john",
product: "spark",
});
}
res.json({});
},
);Réponse avec des outils de fonction
Ajoutez des outils afin que l'IA puisse appeler vos API pendant la conversation :
{
"prompt": "You are a booking assistant. Use the available tools to help customers schedule appointments.",
"voice": "john",
"product": "spark",
"tools": [
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"service": { "type": "string" }
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": {
"X-Api-Key": "your-key"
}
}
}
]
}Aide-mémoire des niveaux de produit
| Produit | Latence | Raisonnement | Acquiescement |
|---|---|---|---|
spark | La plus faible | Basique | — |
bolt | Faible | Amélioré | — |
storm-base | Moyenne | Solide | — |
storm-base-with-ack | Moyenne | Solide | Remplissage automatique pendant la réflexion |
storm-extra | Plus élevée | Approfondi | — |
storm-extra-with-ack | Plus élevée | Approfondi | Remplissage automatique pendant la réflexion |
Associé
L’événement de fin d’appel non bloquant.
Schéma JSON complet pour tools[] et le contrat d’endpoint signé.
Abonnez plusieurs URL à telephony.incoming / web.incoming.
Modèles pour les prompts, outils et tests A/B par appelant.