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

Operations

Vérifier les signatures des webhooks

Chaque requête de webhook et d’outil envoyée par ThunderPhone est signée. Vérifiez la signature une fois à l’aide de la méthode ci-dessous, puis réutilisez la même vérification sur chaque endpoint que vous exécutez.

Chaque requête que nous envoyons à votre serveur — livraisons de webhooks et appels de points de terminaison d’outils — contient une signature HMAC-SHA256 dans l’en-tête X-ThunderPhone-Signature. Configurez la vérification correctement une seule fois, puis utilisez le même helper dans chaque gestionnaire.

L’algorithme

  1. Lisez le corps de requête brut — les octets exacts que nous vous avons envoyés par POST.
  2. Calculez hmac_sha256(secret, body).hexdigest().
  3. Comparez en temps constant avec X-ThunderPhone-Signature. (Une comparaison naïve de chaînes divulgue des informations de temporisation.)

Nous signons exactement les octets que nous transmettons ; la vérification du corps brut fonctionne donc toujours. Ces octets correspondent également à la sérialisation JSON canonique de la charge utile — clés triées par ordre alphabétique, séparateurs compacts (, et : sans espaces), UTF-8. Cela vous offre une seconde méthode totalement équivalente lorsque votre framework n’expose que le JSON analysé : resérialisez de manière canonique et calculez le HMAC sur ce résultat.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

Préférez le corps brut — cela évite une étape et vous protège des particularités d’aller-retour des nombres JSON dans certains langages.

Quel secret ?

SourceSecret
Point de terminaison webhook (/v1/developer/webhook-endpoints)secret par point de terminaison (48 caractères hexadécimaux), renvoyé une seule fois lors de la création
Webhook hérité à URL uniquesecret par organisation renvoyé par GET /v1/webhook
Appel de point de terminaison d’outil (appel direct à votre endpoint.url)Le secret webhook au niveau de l’organisation (le même que pour le webhook hérité à URL unique) — pas un secret par point de terminaison

Stockez le secret dans votre gestionnaire de secrets ou une variable d’environnement — ne le validez jamais dans votre dépôt.

Implémentations de référence

Les quatre vérifient le corps de requête brut :

Python
import hashlib
import hmac
 
 
def verify(body: bytes, signature: str, secret: str) -> bool:
    """Constant-time HMAC-SHA256 verification."""
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
Node.js
import crypto from "node:crypto";
 
export function verify(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),
  );
}
Go
package webhook
 
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func Verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Configuration spécifique au framework

FastAPI
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()           # raw bytes, NOT request.json()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        raise HTTPException(status_code=401)
 
    import json
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
 
const app = express();
 
app.post(
  "/thunderphone-webhook",
  // IMPORTANT: parse as raw; do NOT use express.json() here.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verify(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);
  },
);
Django
import json
 
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
 
 
@csrf_exempt
@require_POST
def hook(request):
    body = request.body  # raw bytes
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

Vérification des appels d'outils

Lorsque l'agent appelle directement l'un de vos outils de fonction (l'outil possède un endpoint), la requête contient deux en-têtes ThunderPhone en plus de vos endpoint.headers configurés :

  • X-ThunderPhone-Call-ID — l'identifiant numérique de l'appel en cours.
  • X-ThunderPhone-Signature — HMAC-SHA256, avec comme clé votre secret webhook au niveau de l'organisation, calculé sur les octets exacts du corps de la requête.

Le même helper verify() fonctionne sans modification, avec deux particularités :

  1. Les outils GET / DELETE n'ont pas de corps. Les arguments sont transmis sous forme de paramètres de requête, et la signature est calculée sur la chaîne d'octets vide — donc verify(b"", sig, secret) (Python) ou verify(Buffer.alloc(0), sig, secret) (Node). Ne hachez pas la chaîne de requête.
  2. Les organisations sans webhook historique configuré n'ont pas de secret d'organisation. Dans ce cas, les appels d'outils contiennent uniquement X-ThunderPhone-Call-ID et aucun en-tête de signature. Configurez le webhook historique (PUT /v1/webhook) pour obtenir un secret de signature, ou authentifiez les appels d'outils avec votre propre en-tête via endpoint.headers.
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...

La distribution d'outils en mode webhook (outils sans endpoint, envoyés à votre webhook d'organisation sous la forme telephony.tool / web.tool) est un webhook signé classique — la procédure standard ci-dessus s'applique. Consultez Outils de fonction pour les deux formats de requête.

Pièges courants

Nouvelle sérialisation avec le formatage par défaut

Analyser le corps puis le réexporter avec les paramètres par défaut de votre bibliothèque JSON (espaces après , / :, clés dans l'ordre d'insertion) produit des octets différents et invalide le HMAC. Vérifiez le corps brut — ou, si vous devez le re-sérialiser, respectez exactement notre forme canonique : clés triées, séparateurs compacts, UTF-8.

Le framework analyse automatiquement le JSON

Le middleware express.json() d'Express consomme le flux du corps et vous perdez les octets bruts. Utilisez express.raw() spécifiquement sur la route de webhook, ou mettez en mémoire tampon le corps brut dans un pré-middleware. Même chose avec NestJS / Koa — consultez leur documentation sur le « corps brut ».

Comparaison non sécurisée contre les attaques temporelles

expected === signature en JS ou expected == signature en Python sont des comparaisons dont la durée varie. Utilisez crypto.timingSafeEqual ou hmac.compare_digest respectivement. La différence de performances est nulle.

Mauvais secret pour les points de terminaison d'outils

Les appels directs aux points de terminaison d'outils sont signés avec le secret de webhook au niveau de l'organisation (GET /v1/webhook) — et non avec un secret propre à chaque point de terminaison provenant de /v1/developer/webhook-endpoints. Réutilisez la même fonction verify(), mais assurez-vous de lui fournir le secret de l'organisation sur les routes d'outils.

Hachage de la chaîne de requête pour les outils GET/DELETE

Pour les méthodes d'outils sans corps, la signature couvre la chaîne d'octets vide, ce qui conserve une recette universelle : appliquez le HMAC au corps brut de la requête, quel qu'il soit. Hacher l'URL ou la chaîne de requête ne correspondra jamais.

Ne pas renvoyer 401 en cas de non-correspondance

Renvoyer 200 lorsque la vérification échoue fait du gestionnaire une cible de rejeu. Répondez toujours avec un code autre que 2xx si la vérification échoue.


Étapes suivantes