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

Webhooks

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, 408 ou 429, 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éponse 2xx dont 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ête X-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"
  }
}
ChampTypeDescription
call_idintegerID de l'appel — stable sur tous les événements de cet appel
from_numberstringNuméro de l'appelant au format E.164
to_numberstringDestination 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"
  }
}
ChampTypeDescription
call_identierID de l’appel
origin_domainchaîneL’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_prefixchaînePremiers caractères de la clé publiable qui a ouvert la session
language, primary_languagechaînePrésents lorsque la session du widget a demandé une surcharge de langue
voicechaînePrésent lorsque la session du widget a demandé une surcharge de voix
website_contextchaînePré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":   []
}
ChampTypeObligatoireDescription
promptchaîneouiPrompt système qui pilote l’agent
voicechaîneouiID 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
productchaînenonValeur par défaut : spark. Valeurs autorisées : spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack
thinking_levelchaînenonminimal, base (par défaut) ou extra. Remplacé pour les produits Storm : storm-extra* force extra, les autres storm-* forcent base
audio_context_modechaînenonfull (par défaut) ou reduced
watchdog_enabledbooléennonActive la supervision pour cet appel. Valeur par défaut : false
additional_audio_contextbooléen | nullnonInclut 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_modechaînenonnone, acknowledgement (par défaut) ou tick
languagechaînenonRaccourci pour primary_language
primary_languagechaînenonCode de langue, normalisé (valeur par défaut : en). Les codes non résolubles rejettent l’appel
has_additional_languagesbooléennonValeur par défaut : false
additional_languagestableau de chaînesnonLangues supplémentaires vers lesquelles l’agent peut basculer
native_voice_switchingbooléennonValeur 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_switchingchaîne | nullnonask_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_trackchaîne | nullnonID d’audio ambiant ou null
acknowledgement_prompt_modechaînenonauto (par défaut) ou manual (produits Storm-with-ack)
acknowledgement_promptchaînenonUtilisé lorsque acknowledgement_prompt_mode="manual"
silence_interval_secondsentier | nullnon5–120. Secondes de silence de l’appelant avant une relance
silence_max_checkinsentier | nullnon1–10
silence_checkins_enabledbooléennonValeur par défaut : true
connect_tone_enabledbooléennonValeur par défaut : false
pronunciation_dictsbooléen | nullnonValeur 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_actionchaînenonprompt (par défaut), hangup ou message
voicemail_messagechaînenonUtilisé lorsque voicemail_action="message"
agent_namechaînenonNom d’affichage transmis aux tableaux de bord et au widget
org_namechaînenonNom d’affichage de l’organisation pour le persona de l’agent
toolstableaunonSchémas d’outils de fonction intégrés (voir Outils de fonction)
transfer_destinationstableau d’objets | nullnonJusqu’à 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
timezonechaînenonFuseau 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_identiernonÉ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

Python (FastAPI)
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 {}
Node.js (Express)
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

ProduitLatenceRaisonnementAcquiescement
sparkLa plus faibleBasique—
boltFaibleAmélioré—
storm-baseMoyenneSolide—
storm-base-with-ackMoyenneSolideRemplissage automatique pendant la réflexion
storm-extraPlus élevéeApprofondi—
storm-extra-with-ackPlus élevéeApprofondiRemplissage automatique pendant la réflexion

Associé