> ## 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.

# Verifica le firme dei webhook

> Ogni richiesta di webhook e strumento da ThunderPhone è firmata. Verifica una volta; riutilizza ovunque.

Ogni richiesta che inviamo al tuo server — consegne webhook e
invocazioni degli endpoint degli strumenti — contiene una firma HMAC-SHA256 nell'header
`X-ThunderPhone-Signature`. Implementa correttamente la verifica una volta e
integra lo stesso helper in ogni handler.

## L'algoritmo

1. Leggi il corpo **raw** della richiesta — gli esatti byte che ti abbiamo inviato tramite POST.
2. Calcola `hmac_sha256(secret, body).hexdigest()`.
3. Confrontalo in **tempo costante** con `X-ThunderPhone-Signature`.
   (Un confronto ingenuo tra stringhe espone informazioni di timing.)

Firmiamo esattamente i byte che trasmettiamo, quindi la verifica del corpo raw
funziona sempre. Quei byte sono anche la **serializzazione JSON canonica**
del payload — chiavi ordinate alfabeticamente, separatori compatti
(`,` e `:` senza spazi), UTF-8. Questo ti offre una seconda procedura,
del tutto equivalente, quando il framework espone solo JSON già analizzato:
serializzalo nuovamente in modo canonico e calcola l'HMAC su quello.

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

Preferisci il corpo raw: è un passaggio in meno ed evita le anomalie di
conversione dei numeri JSON in alcuni linguaggi.

## Quale secret?

| Origine                                                                                                  | Secret                                                                                                                        |
| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [Endpoint webhook](/it/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                           | `secret` per endpoint (48 caratteri esadecimali) restituito una sola volta alla creazione                                     |
| [Webhook legacy con URL singolo](/api-reference/organizations#legacy-single-url-webhook)                 | `secret` per organizzazione restituito con `GET /v1/webhook`                                                                  |
| [Invocazione dell'endpoint degli strumenti](/it/tools/overview) (chiamata diretta al tuo `endpoint.url`) | Il **secret webhook a livello di organizzazione** (lo stesso del webhook legacy con URL singolo) — non un secret per endpoint |

Conserva il secret nel tuo gestore di segreti o in una variabile d'ambiente — non eseguire mai il commit.

## Implementazioni di riferimento

Tutte e quattro verificano il corpo raw della richiesta:

<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>

## Configurazione specifica per 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>

## Verifica delle chiamate degli strumenti

Quando l'agente richiama direttamente uno dei tuoi
[strumenti funzione](/it/tools/overview) (lo strumento dispone di un
`endpoint`), la richiesta include due intestazioni ThunderPhone oltre
alle `endpoint.headers` configurate:

* `X-ThunderPhone-Call-ID` — l'ID numerico della chiamata in corso.
* `X-ThunderPhone-Signature` — HMAC-SHA256, con chiave costituita dal tuo
  **segreto webhook a livello di organizzazione**, calcolato sui byte
  esatti del corpo della richiesta.

Lo stesso helper `verify()` funziona senza modifiche, con due particolarità:

1. **Gli strumenti `GET` / `DELETE` non hanno corpo.** Gli argomenti vengono passati come
   parametri di query e la firma viene calcolata sulla **stringa di byte
   vuota** — quindi `verify(b"", sig, secret)` (Python) oppure
   `verify(Buffer.alloc(0), sig, secret)` (Node). **Non** calcolare l'hash della
   stringa di query.
2. **Le organizzazioni senza un webhook legacy configurato non hanno un segreto dell'organizzazione.** In
   questo caso, le chiamate degli strumenti includono solo `X-ThunderPhone-Call-ID` e nessuna
   intestazione della firma. Configura il webhook legacy
   (`PUT /v1/webhook`) per ottenere un segreto di firma, oppure autentica le chiamate degli strumenti
   con una tua intestazione tramite `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)
    ...
```

L'inoltro degli strumenti in **modalità** webhook (strumenti senza un `endpoint`, inviati
al webhook della tua organizzazione come `telephony.tool` / `web.tool`) è un normale
webhook firmato: si applica la procedura standard descritta sopra. Consulta
[Strumenti funzione](/it/tools/overview) per entrambi i formati di richiesta.

## Problemi comuni

<AccordionGroup>
  <Accordion title="Ri-serializzazione con la formattazione predefinita">
    Analizzare il body e riesportarlo con le impostazioni predefinite
    della libreria JSON (spazi dopo `,` / `:`, chiavi ordinate per inserimento) produce
    byte diversi e compromette l'HMAC. Verifica il body grezzo oppure, se
    devi ri-serializzarlo, usa esattamente la nostra forma canonica: chiavi
    ordinate, separatori compatti, UTF-8.
  </Accordion>

  <Accordion title="Il framework analizza automaticamente JSON">
    Il middleware `express.json()` di Express consuma lo stream del body
    e perdi i byte grezzi. Usa `express.raw()` specificamente sulla route
    webhook oppure memorizza nel buffer il body grezzo in un pre-middleware.
    Lo stesso vale per NestJS / Koa: consulta la loro documentazione sul "raw body".
  </Accordion>

  <Accordion title="Confronto non sicuro rispetto ai tempi">
    `expected === signature` in JS o `expected == signature` in
    Python sono confronti con tempi variabili. Usa `crypto.timingSafeEqual`
    o `hmac.compare_digest` rispettivamente. La differenza di prestazioni
    è nulla.
  </Accordion>

  <Accordion title="Secret errato per gli endpoint degli strumenti">
    Le chiamate dirette agli endpoint degli strumenti sono firmate con il **secret webhook
    a livello di organizzazione** (`GET /v1/webhook`), non con alcun secret per endpoint
    da `/v1/developer/webhook-endpoints`. Riutilizza la stessa funzione `verify()`,
    ma assicurati di fornirle il secret dell'organizzazione nelle route degli strumenti.
  </Accordion>

  <Accordion title="Hash della stringa di query negli strumenti GET/DELETE">
    Per i metodi degli strumenti senza body, la firma copre la stringa di byte
    vuota, mantenendo un'unica procedura universale: calcola l'HMAC del body grezzo
    della richiesta, qualunque esso sia. L'hash dell'URL o della stringa di query non corrisponderà mai.
  </Accordion>

  <Accordion title="Mancata restituzione di 401 in caso di mancata corrispondenza">
    Restituire 200 quando la verifica fallisce rende l'handler un bersaglio per replay.
    Rispondi sempre con un codice diverso da 2xx se la verifica fallisce.
  </Accordion>
</AccordionGroup>

***

## Passaggi successivi

<CardGroup cols={2}>
  <Card title="Panoramica dei webhook" icon="bolt" href="/it/webhooks/overview">
    Semantica di consegna, tentativi, IP di origine.
  </Card>

  <Card title="Endpoint webhook" icon="plug" href="/it/webhooks/endpoints">
    Gestisci più URL, ruota i secret.
  </Card>

  <Card title="Function Tools" icon="screwdriver-wrench" href="/it/tools/overview">
    I due percorsi di invocazione degli strumenti e le relative strutture delle richieste.
  </Card>

  <Card title="Integrazioni degli strumenti" icon="wrench" href="/it/guides/build-tool-integration">
    Crea un'integrazione completa supportata da strumenti, dall'inizio alla fine.
  </Card>
</CardGroup>
