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

Webhooks

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 :

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

  1. Lisez le corps brut de la requête avant toute analyse.
  2. Calculez hmac_sha256(secret, body).hexdigest().
  3. 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.

Python
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 "", 204
Node.js (Express)
import 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'URL1 par organisationPlusieurs par organisation
Couverture des événementstelephony.* / web.* uniquementLes 10 types d'événements
Filtre d'événementsPar point de terminaison
Nouvelles tentativesAucune8 tentatives sur 24 h
Enveloppetype + datatype + data + event_id
Rotation des secretsRemplace le secret uniqueSecret par point de terminaison
Désactivation sans suppressionstatus=disabled
Visibilité de l'étatactive / disabled / failing
Échange de configuration bloquantOui (telephony.incoming / web.incoming)Jamais — notifications uniquement
Idéal pourConfiguration dynamique des appelsConsommation 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é