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
telephony.incoming / web.incoming bloquante à votre
URL de webhook héritée
et attend jusqu'à 10 secondes une réponse de configuration. 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.
L'échange bloquant n'a aucun repli : si votre gestionnaire renvoie un statut
non-2xx, expire ou renvoie une configuration qui échoue à la validation,
l'appel est rejeté (l'appel téléphonique ne se connecte pas ; la requête de
session du widget échoue avec 502/422). Répondez rapidement — l'appelant
entend la tonalité de retour pendant votre décision.
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 | integer | ID de l'appel |
origin_domain | string | Origine de la page qui héberge le widget |
publishable_key_prefix | string | Premiers caractères de la clé publiable ayant ouvert la session |
language, primary_language | string | Présent lorsque la session du widget a demandé un remplacement de langue |
voice | string | Présent lorsque la session du widget a demandé un remplacement de voix |
website_context | string | Présent lorsque le widget a transmis un contexte de page par session |
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 | string | oui | Prompt système qui pilote l’agent |
voice | string | oui | Identifiant vocal provenant de GET /v1/voices, par exemple john. voice_name est accepté comme alias. Les voix inconnues échouent à la validation et refusent l’appel |
product | string | non | La valeur par défaut est spark. Valeurs autorisées : spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | string | 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 | string | non | full (par défaut) ou reduced |
watchdog_enabled | boolean | non | Activez la supervision pour cet appel. Valeur par défaut : false |
additional_audio_context | boolean | 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 riches en orthographe ou en nombres, 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 | string | non | none, acknowledgement (par défaut) ou tick |
language | string | non | Raccourci pour primary_language |
primary_language | string | non | Code de langue, normalisé (valeur par défaut : en). Les codes non résolubles refusent l’appel |
has_additional_languages | boolean | non | Valeur par défaut : false |
additional_languages | array of string | non | Langues supplémentaires vers lesquelles l’agent peut basculer |
native_voice_switching | boolean | non | Valeur par défaut : false. Lorsque l’appel bascule vers une autre langue, remplace la voix par une voix native de cette langue (correspondant au genre) au lieu de conserver la voix configurée |
background_track | string | null | non | Identifiant audio d’ambiance ou null |
acknowledgement_prompt_mode | string | non | auto (par défaut) ou manual (produits Storm-with-ack) |
acknowledgement_prompt | string | non | Utilisé lorsque acknowledgement_prompt_mode="manual" |
silence_interval_seconds | integer | null | non | 5 à 120. Secondes de silence de l’appelant avant une vérification |
silence_max_checkins | integer | null | non | 1 à 10 |
silence_checkins_enabled | boolean | non | Valeur par défaut : true |
connect_tone_enabled | boolean | non | Valeur par défaut : false |
voicemail_action | string | non | prompt (par défaut), hangup ou message |
voicemail_message | string | non | Utilisé lorsque voicemail_action="message" |
agent_name | string | non | Nom d’affichage transmis aux dashboards et au widget |
org_name | string | non | Nom d’affichage de l’organisation pour le persona de l’agent |
tools | array | non | Schémas de function tools intégrés (voir Function Tools) |
call_id | integer | non | Écho facultatif de l’identifiant d’appel de la requête ; ignoré |
Comme prompt et voice sont obligatoires, renvoyer {} ou toute
réponse qui échoue à la validation refuse l’appel avec 422 : il n’y a
aucun repli sur un agent statique sur cette route (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.