Présentation des webhooks
Comment ThunderPhone transmet des événements en temps réel, comment vérifier les signatures et comment se comparent les modèles de transmission hérités et basés sur des endpoints.
ThunderPhone envoie des requêtes HTTP POST à votre serveur lorsque des événements
se produisent pendant un appel — un appel entrant commence, un appel se termine, une exécution
d'évaluation se termine, une alerte se déclenche, etc. Il existe deux modèles
de livraison :
Plusieurs URL, des secrets par point de terminaison, des filtres d'événements par point de terminaison,
et des nouvelles tentatives automatiques.
Gérez-les via GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints.
Une URL par organisation. Transporte les événements du cycle de vie des appels, y compris les
échanges de configuration bloquants. Géré avec GET/PUT /v1/webhook.
Les dix types d'événements du catalogue des événements sont
livrés via des points de terminaison webhook. Les six événements du cycle de vie des appels
(telephony.incoming, telephony.complete, telephony.tool,
web.incoming, web.complete, web.tool) sont également envoyés au
webhook historique à URL unique — si vous disposez à la fois d'une URL historique et d'un
point de terminaison correspondant, vous recevez l'événement sur les deux chemins.
Le comportement bloquant (l'échange de configuration
telephony.incoming / web.incoming et la
répartition des outils en mode webhook)
existe exclusivement sur le chemin historique ; chaque livraison à un point de terminaison est une
notification envoyée sans attente de réponse.
Format de la charge utile
Les livraisons aux points de terminaison sont un objet JSON avec data, event_id et
type :
{
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
},
"event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
"type": "telephony.incoming"
}event_id est unique pour chaque événement émis. Il est identique lors des nouvelles tentatives
et sur chaque point de terminaison qui reçoit l'événement — dédupliquez à l'aide de cet identifiant.
Le webhook historique à URL unique envoie les mêmes type et data, mais
sans event_id :
{
"type": "telephony.incoming",
"data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}Sur le réseau, chaque corps est sérialisé de manière canonique — clés triées par ordre alphabétique, sans espaces, UTF-8. Les exemples mis en forme dans ces Docs sont uniquement destinés à faciliter la lecture.
Consultez le catalogue des événements pour obtenir la liste complète des types d'événements et des champs de charge utile.
Vérification de signature
Chaque requête comporte une signature HMAC-SHA256 calculée sur le corps brut de la requête dans l’en-tête X-ThunderPhone-Signature. La clé de signature est le secret du point de terminaison (ou le secret de webhook au niveau de votre organisation pour les livraisons héritées).
Étapes
- Lisez le corps brut de la requête avant toute analyse.
- Calculez
hmac_sha256(secret, body).hexdigest(). - Comparez-le en temps constant à l’en-tête
X-ThunderPhone-Signature.
Nous signons exactement les octets que nous transmettons, et ces octets correspondent à la sérialisation JSON canonique (clés triées, séparateurs compacts). La vérification par rapport au corps brut fonctionne donc toujours — et si votre framework ne vous fournit que du JSON analysé, le re-sérialiser avec des clés triées et des séparateurs compacts produit des octets identiques. Les deux méthodes sont décrites dans le guide de vérification.
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature or "")
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/thunderphone-webhook")
def handle():
body = request.get_data()
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify_signature(body, sig, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
# dispatch on event["type"] …
return "", 204import crypto from "node:crypto";
import express from "express";
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (!signature || expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
const app = express();
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("X-ThunderPhone-Signature") || "";
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// dispatch on event.type …
res.sendStatus(204);
},
);Sémantique de livraison
Cette sémantique s'applique aux livraisons vers des points de terminaison. Le webhook historique à URL unique consiste en une seule tentative synchrone sans nouvelle tentative.
Nouvelles tentatives
Chaque événement fait l'objet d'une tentative immédiate. Toute réponse 2xx
confirme la livraison. Dans tous les autres cas (réponse non-2xx,
erreur de connexion, délai d'expiration), nous effectuons de nouvelles tentatives 1 min, 5 min, 30 min, 2 h, 6 h,
12 h et 24 h après la première tentative — soit 8 tentatives sur
24 heures. Si toutes les tentatives échouent, la livraison s'arrête et le point de terminaison
est marqué status="failing" dans les
points de terminaison webhook. Renvoyez 2xx dès que
la charge utile est acceptée de manière durable ; traitez-la de façon asynchrone.
Ordre
L'ordre de livraison est assuré au mieux. En pratique, nous livrons les événements dans
leur ordre d'émission, mais les nouvelles tentatives peuvent modifier cet ordre en cas d'échec.
Dédupliquez et réconciliez toujours à l'aide de call_id / l'identifiant d'objet.
Doublons
La livraison est au moins une fois : une nouvelle tentative après une réponse que nous n'avons jamais
reçue peut dupliquer un événement. Chaque nouvelle tentative porte le même
event_id ; stockez donc les identifiants traités et ignorez les doublons. event_id est
également partagé entre les points de terminaison — deux points de terminaison abonnés au
même événement reçoivent le même event_id.
Délais d'expiration
Les livraisons vers des points de terminaison ont un délai d'expiration de 30 s par tentative. Sur le
chemin historique, les requêtes bloquantes qui déterminent le comportement des appels en direct — l'échange de configuration
telephony.incoming / web.incoming —
expirent après 10 s, mais une réponse lente retarde la prise d'appel ;
visez donc une réponse en quelques secondes. La répartition d'outils en mode webhook
autorise 20 s par défaut, et les déclarations d'outils peuvent définir un timeout
de niveau supérieur.
Adresses IP source
Les webhooks sortants proviennent de la plage d'adresses IP cloud de ThunderPhone. Si votre pare-feu nécessite une liste d'autorisation, contactez le support et nous vous communiquerons les plages actuelles.
Choisir entre les webhooks historiques et basés sur des points de terminaison
| Fonctionnalité | Historique (/v1/webhook) | Points de terminaison (/v1/developer/webhook-endpoints) |
|---|---|---|
| Nombre d'URL | 1 par organisation | Plusieurs par organisation |
| Couverture des événements | telephony.* / web.* uniquement | Les 10 types d'événements |
| Filtre d'événements | — | Par point de terminaison |
| Nouvelles tentatives | Aucune | 8 tentatives sur 24 h |
| Enveloppe | type + data | type + data + event_id |
| Rotation des secrets | Remplace le secret unique | Secret par point de terminaison |
| Désactivation sans suppression | — | status=disabled |
| Visibilité de l'état | — | active / disabled / failing |
| Échange de configuration bloquant | Oui (telephony.incoming / web.incoming) | Jamais — notifications uniquement |
| Idéal pour | Configuration dynamique des appels | Consommation d'événements en production |
Les nouvelles intégrations doivent consommer les événements via des webhooks basés sur des points de terminaison. Conservez (ou ajoutez) une URL historique uniquement si vous configurez les appels dynamiquement au moment de la prise d'appel ou utilisez la répartition d'outils en mode webhook — ces échanges requête/réponse ne s'exécutent que sur le chemin historique.
Associé
Tous les types d'événements et leurs charges utiles.
Gérez plusieurs points de terminaison, filtres d'événements et secrets.
La requête bloquante à laquelle votre serveur doit répondre pour configurer les appels.
Charge utile après appel avec transcription, enregistrement et métriques.