ThunderPhone 2.0 ya está disponible.Empieza por tu cuenta desde 2¢/min.Lee el anuncio

Operations

Verificar firmas de webhooks

Cada solicitud de webhook y herramienta que envía ThunderPhone está firmada. Verifica la firma una vez con la receta de esta página y luego reutiliza la misma verificación en cada endpoint que ejecutes.

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. Configura la verificación correctamente una vez y conecta el mismo asistente en cada controlador.

El algoritmo

  1. Lee el cuerpo de la solicitud sin procesar — 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 tiempo.)

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 completamente equivalente cuando tu framework solo expone JSON analizado: vuelve a serializar de forma canónica y calcula el HMAC de eso.

# 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 las particularidades del recorrido de ida y vuelta de los números JSON en algunos lenguajes.

¿Qué secreto?

OrigenSecreto
Endpoint de webhook (/v1/developer/webhook-endpoints)secret por endpoint (48 caracteres hexadecimales) que se devuelve una sola vez al crearlo
Webhook heredado de URL únicasecret por organización devuelto en GET /v1/webhook
Invocación de endpoint de herramienta (llamada directa a tu endpoint.url)El secreto de webhook a nivel de organización (el mismo que el del webhook heredado de URL única) — no un secreto por endpoint

Guarda el secreto en tu gestor de secretos o variable de entorno — nunca lo incluyas en un commit.

Implementaciones de referencia

Las cuatro verifican el cuerpo de la solicitud sin procesar:

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

Configuración específica del 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})

Verificación de llamadas a herramientas

Cuando el agente invoca directamente una de tus herramientas de función (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 en curso.
  • X-ThunderPhone-Signature: HMAC-SHA256, con tu secreto de webhook de nivel de organización como clave, sobre los bytes exactos del cuerpo de la solicitud.

El mismo asistente verify() funciona sin cambios, con dos detalles:

  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; por lo tanto, usa 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 un secreto de organización. En ese caso, las llamadas a herramientas solo incluyen X-ThunderPhone-Call-ID y no incluyen un 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.
@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 distribución 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 normal: aplica la receta estándar anterior. Consulta Herramientas de función para ver ambas formas de solicitud.

Errores comunes

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 ordenadas por inserción) produce bytes diferentes e invalida el HMAC. Verifica el cuerpo sin procesar; o, si debes volver a serializarlo, reproduce exactamente nuestra forma canónica: claves ordenadas, separadores compactos, UTF-8.

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 "cuerpo sin procesar".

Comparación no segura frente a ataques de temporización

expected === signature en JS o expected == signature en Python son comparaciones cuyo tiempo de ejecución varía. Usa crypto.timingSafeEqual o hmac.compare_digest, respectivamente. La diferencia de rendimiento es nula.

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.

Aplicar hash a la cadena de consulta en herramientas GET/DELETE

Para 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. Aplicar hash a la URL o a la cadena de consulta nunca coincidirá.

No devolver 401 cuando no hay coincidencia

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.


Próximos pasos