> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Vérifier les signatures des webhooks

> Chaque requête de webhook et d'outil provenant de ThunderPhone est signée. Vérifiez une fois, réutilisez partout.

Chaque requête que nous envoyons à votre serveur — livraisons de webhooks et
invocations 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 et
réutilisez le même utilitaire 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 expose 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 deuxième méthode, entièrement
équivalente, lorsque votre framework n'expose que le JSON analysé :
resérialisez de manière canonique et calculez le HMAC de ce résultat.

```python theme={null}
# 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 supprime les particularités de conversion
aller-retour des nombres JSON dans certains langages.

## Quel secret ?

| Source                                                                                                 | Secret                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Point de terminaison webhook](/fr/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)             | `secret` propre à chaque point de terminaison (48 caractères hexadécimaux), renvoyé une seule fois lors de la création                                   |
| [Webhook historique à URL unique](/api-reference/organizations#legacy-single-url-webhook)              | `secret` propre à l'organisation, renvoyé par `GET /v1/webhook`                                                                                          |
| [Invocation de point de terminaison d'outil](/fr/tools/overview) (appel direct à votre `endpoint.url`) | Le **secret webhook au niveau de l'organisation** (le même que pour le webhook historique à URL unique) — pas un secret propre à un 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 :

<CodeGroup>
  ```python Python theme={null}
  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 "")
  ```

  ```javascript Node.js theme={null}
  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 Go theme={null}
  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 Ruby theme={null}
  require "openssl"

  def verify(body, signature, secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
    Rack::Utils.secure_compare(expected, signature.to_s)
  end
  ```
</CodeGroup>

## Configuration spécifique au framework

<CodeGroup>
  ```python FastAPI theme={null}
  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}
  ```

  ```javascript Express theme={null}
  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);
    },
  );
  ```

  ```python Django theme={null}
  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})
  ```
</CodeGroup>

## Vérifier les appels d'outils

Lorsque l'agent invoque directement l'un de vos
[outils de fonction](/fr/tools/overview) (l'outil possède un
`endpoint`), la requête inclut 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, utilisant comme clé votre
  **secret de webhook au niveau de l'organisation**, sur les octets exacts
  du corps de la requête.

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

1. **Les outils `GET` / `DELETE` n'ont pas de corps.** Les arguments sont transmis
   en tant que 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 hérité configuré n'ont pas de secret d'organisation.**
   Dans ce cas, les appels d'outils incluent uniquement `X-ThunderPhone-Call-ID` et aucun
   en-tête de signature. Configurez le webhook hérité
   (`PUT /v1/webhook`) pour obtenir un secret de signature, ou authentifiez les appels
   d'outils avec votre propre en-tête via `endpoint.headers`.

```python theme={null}
@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 des outils en mode webhook (outils sans `endpoint`, transmis
à 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](/fr/tools/overview) pour les deux formats de requête.

## Pièges courants

<AccordionGroup>
  <Accordion title="Resé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 resérialiser, reproduisez exactement notre forme canonique : clés
    triées, séparateurs compacts, UTF-8.
  </Accordion>

  <Accordion title="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
    du webhook, ou mettez en mémoire tampon le corps brut dans un pré-middleware.
    Même principe pour NestJS / Koa — consultez leur documentation sur le « corps brut ».
  </Accordion>

  <Accordion title="Comparaison non sûre vis-à-vis du timing">
    `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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Hachage de la chaîne de requête sur 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. Le hachage de l’URL ou de la chaîne de requête ne correspondra jamais.
  </Accordion>

  <Accordion title="Ne pas renvoyer 401 en cas de non-correspondance">
    Renvoyer 200 lorsqu’une vérification échoue fait du gestionnaire une cible de rejeu.
    Répondez toujours avec un statut autre que 2xx si la vérification échoue.
  </Accordion>
</AccordionGroup>

***

## Étapes suivantes

<CardGroup cols={2}>
  <Card title="Vue d’ensemble des webhooks" icon="bolt" href="/fr/webhooks/overview">
    Sémantique de livraison, tentatives, adresses IP source.
  </Card>

  <Card title="Points de terminaison des webhooks" icon="plug" href="/fr/webhooks/endpoints">
    Gérez plusieurs URL, faites tourner les secrets.
  </Card>

  <Card title="Outils de fonction" icon="screwdriver-wrench" href="/fr/tools/overview">
    Les deux modes d’invocation d’outils et les formats de leurs requêtes.
  </Card>

  <Card title="Intégrations d’outils" icon="wrench" href="/fr/guides/build-tool-integration">
    Créez une intégration complète basée sur des outils, de bout en bout.
  </Card>
</CardGroup>
