> ## 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 las firmas de los webhooks

> Cada webhook y solicitud de herramienta de ThunderPhone está firmada. Verifica una vez; reutiliza en todas partes.

Cada solicitud que enviamos a tu servidor —entregas de webhooks e invocaciones de endpoints de herramientas— incluye una firma HMAC-SHA256 en el encabezado `X-ThunderPhone-Signature`. Implementa la verificación correctamente una vez y reutiliza el mismo helper en cada controlador.

## El algoritmo

1. Lee el cuerpo **sin procesar** de la solicitud: los bytes exactos que te enviamos mediante POST.
2. Calcula `hmac_sha256(secret, body).hexdigest()`.
3. Compara en **tiempo constante** con `X-ThunderPhone-Signature`.
   (Una comparación de cadenas ingenua filtra información de temporización).

Firmamos exactamente los bytes que transmitimos, por lo que verificar el cuerpo sin procesar siempre funciona. Esos bytes también son la **serialización JSON canónica** de la carga útil: claves ordenadas alfabéticamente, separadores compactos
(`,` y `:` sin espacios), UTF-8. Esto te ofrece una segunda receta totalmente
equivalente cuando tu framework solo expone JSON analizado:
vuelve a serializar canónicamente y calcula el HMAC de eso.

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

Prefiere el cuerpo sin procesar: es un paso menos y evita peculiaridades de
conversión de números JSON de ida y vuelta en algunos lenguajes.

## ¿Qué secreto?

| Origen                                                                                            | Secreto                                                                                                                           |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| [Endpoint de webhook](/es/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)                 | `secret` por endpoint (48 caracteres hexadecimales) devuelto una sola vez al crearlo                                              |
| [Webhook heredado de una sola URL](/api-reference/organizations#legacy-single-url-webhook)        | `secret` por organización devuelto con `GET /v1/webhook`                                                                          |
| [Invocación de endpoint de herramienta](/es/tools/overview) (llamada directa a tu `endpoint.url`) | El **secreto de webhook de nivel de organización** (el mismo que el webhook heredado de una sola URL), no un secreto por endpoint |

Almacena el secreto en tu administrador de secretos o variable de entorno; nunca lo confirmes en el repositorio.

## Implementaciones de referencia

Las cuatro verifican el cuerpo sin procesar de la solicitud:

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

## Integración específica del 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>

## Verificación de llamadas a herramientas

Cuando el agente invoca directamente una de tus
[herramientas de función](/es/tools/overview) (la herramienta tiene un
`endpoint`), la solicitud incluye dos encabezados de ThunderPhone junto
con tus `endpoint.headers` configurados:

* `X-ThunderPhone-Call-ID` — el ID numérico de la llamada activa.
* `X-ThunderPhone-Signature` — HMAC-SHA256, con clave de tu
  **secreto de webhook a nivel de organización**, sobre los bytes exactos
  del cuerpo de la solicitud.

El mismo helper `verify()` funciona sin cambios, con dos particularidades:

1. **Las herramientas `GET` / `DELETE` no tienen cuerpo.** Los argumentos se envían como
   parámetros de consulta y la firma se calcula sobre la **cadena de bytes
   vacía**; es decir, `verify(b"", sig, secret)` (Python) o
   `verify(Buffer.alloc(0), sig, secret)` (Node). **No** calcules el hash de la
   cadena de consulta.
2. **Las organizaciones sin un webhook heredado configurado no tienen secreto de organización.** En
   ese caso, las llamadas a herramientas incluyen solo `X-ThunderPhone-Call-ID` y ningún
   encabezado de firma. Configura el webhook heredado
   (`PUT /v1/webhook`) para obtener un secreto de firma, o autentica las llamadas a
   herramientas con tu propio encabezado mediante `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)
    ...
```

El despacho de herramientas en **modo** webhook (herramientas sin un `endpoint`, enviadas
a tu webhook de organización como `telephony.tool` / `web.tool`) es un webhook
firmado común: se aplica la receta estándar anterior. Consulta
[Herramientas de función](/es/tools/overview) para ambas formas de solicitud.

## Errores comunes

<AccordionGroup>
  <Accordion title="Reserializar con el formato predeterminado">
    Analizar el cuerpo y volver a serializarlo con los valores
    predeterminados de tu biblioteca JSON (espacios después de `,` / `:`, claves en orden de inserción) produce
    bytes diferentes y rompe el HMAC. Verifica el cuerpo sin procesar; o, si
    debes reserializarlo, coincide exactamente con nuestra forma canónica: claves
    ordenadas, separadores compactos, UTF-8.
  </Accordion>

  <Accordion title="El framework analiza JSON automáticamente">
    El middleware `express.json()` de Express consume el flujo del cuerpo
    y pierdes los bytes sin procesar. Usa `express.raw()` específicamente en la ruta
    del webhook, o almacena el cuerpo sin procesar en un middleware previo.
    Lo mismo ocurre con NestJS / Koa: consulta su documentación sobre "raw body".
  </Accordion>

  <Accordion title="Comparación no segura frente a ataques de temporización">
    `expected === signature` en JS o `expected == signature` en
    Python son comparaciones con tiempo variable. Usa `crypto.timingSafeEqual`
    o `hmac.compare_digest`, respectivamente. La diferencia de rendimiento
    es nula.
  </Accordion>

  <Accordion title="Secreto incorrecto para endpoints de herramientas">
    Las llamadas directas a endpoints de herramientas se firman con el **secreto
    de webhook a nivel de organización** (`GET /v1/webhook`), no con ningún secreto por endpoint
    de `/v1/developer/webhook-endpoints`. Reutiliza la misma función `verify()`,
    pero asegúrate de proporcionarle el secreto de la organización en las rutas de herramientas.
  </Accordion>

  <Accordion title="Hashear la cadena de consulta en herramientas GET/DELETE">
    Para los métodos de herramientas sin cuerpo, la firma cubre la cadena de bytes
    vacía, manteniendo una única receta universal: aplica HMAC al cuerpo sin procesar de la solicitud,
    sea cual sea. Hashear la URL o la cadena de consulta nunca coincidirá.
  </Accordion>

  <Accordion title="No devolver 401 cuando no coincide">
    Devolver 200 cuando falla la verificación convierte el controlador en un objetivo
    de repetición. Responde siempre con un código distinto de 2xx si la verificación falla.
  </Accordion>
</AccordionGroup>

***

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Descripción general de webhooks" icon="bolt" href="/es/webhooks/overview">
    Semántica de entrega, reintentos e IP de origen.
  </Card>

  <Card title="Endpoints de webhook" icon="plug" href="/es/webhooks/endpoints">
    Administra varias URL y rota secretos.
  </Card>

  <Card title="Herramientas de función" icon="screwdriver-wrench" href="/es/tools/overview">
    Las dos rutas de invocación de herramientas y las formas de sus solicitudes.
  </Card>

  <Card title="Integraciones de herramientas" icon="wrench" href="/es/guides/build-tool-integration">
    Crea una integración completa respaldada por herramientas de principio a fin.
  </Card>
</CardGroup>
