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

Webhooks

Descripción general de webhooks

Cómo ThunderPhone entrega eventos en tiempo real, cómo verificar firmas y cómo se comparan los modelos de entrega heredado y basado en endpoints.

ThunderPhone envía solicitudes HTTP POST a tu servidor cuando ocurren eventos durante una llamada: inicia una llamada entrante, finaliza una llamada, se completa una ejecución de evaluación, se activa una alerta, entre otros. Hay dos modelos de entrega:

Los diez tipos de eventos del catálogo de eventos se entregan mediante endpoints de webhook. Los seis eventos del ciclo de vida de las llamadas (telephony.incoming, telephony.complete, telephony.tool, web.incoming, web.complete, web.tool) también se envían al webhook heredado de URL única: si tienes tanto una URL heredada como un endpoint coincidente, recibes el evento en ambas rutas. El comportamiento bloqueante (el intercambio de configuración de telephony.incoming / web.incoming y el despacho de herramientas en modo webhook) existe exclusivamente en la ruta heredada; cada entrega a un endpoint es una notificación sin espera de respuesta.

Formato de la carga útil

Las entregas a endpoints son un objeto JSON con data, event_id y type:

{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}

event_id es único para cada evento emitido. Es idéntico en todos los reintentos y en cada endpoint que recibe el evento; úsalo para deduplicar.

El webhook heredado de URL única envía el mismo type y data, pero sin event_id:

{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}

En la transmisión, cada cuerpo se serializa de forma canónica: claves ordenadas alfabéticamente, sin espacios en blanco y en UTF-8. Los ejemplos con formato legible de esta documentación son solo para facilitar la lectura.

Consulta el catálogo de eventos para ver la lista completa de tipos de eventos y campos de carga útil.

Verificación de firma

Cada solicitud incluye una firma HMAC-SHA256 sobre el cuerpo sin procesar de la solicitud en el encabezado X-ThunderPhone-Signature. La clave de firma es el secret del endpoint (o el secret de webhook de tu organización para entregas heredadas).

Pasos

  1. Lee el cuerpo sin procesar de la solicitud antes de realizar cualquier análisis.
  2. Calcula hmac_sha256(secret, body).hexdigest().
  3. Compara en tiempo constante con el encabezado X-ThunderPhone-Signature.

Firmamos exactamente los bytes que transmitimos, y esos bytes son la serialización JSON canónica (claves ordenadas, separadores compactos). Por lo tanto, verificar con el cuerpo sin procesar siempre funciona; y si tu framework solo te proporciona JSON analizado, volver a serializarlo con claves ordenadas y separadores compactos genera bytes idénticos. Ambas opciones se explican en la guía de verificación.

Python
import hmac
import hashlib
 
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
 
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
 
@app.post("/thunderphone-webhook")
def handle():
    body = request.get_data()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify_signature(body, sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
 
function verifySignature(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),
  );
}
 
const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verifySignature(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);
  },
);

Semántica de entrega

Estas semánticas se aplican a las entregas de endpoints. El webhook heredado de una sola URL realiza un único intento síncrono sin reintentos.

Reintentos

Cada evento se intenta una vez de inmediato. Cualquier respuesta 2xx confirma la entrega. Ante cualquier otro resultado (que no sea 2xx, error de conexión, tiempo de espera agotado), reintentamos 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h después del primer intento: 8 intentos durante 24 horas. Si todos los intentos fallan, la entrega se detiene y el endpoint se marca como status="failing" en endpoints de webhook. Devuelve 2xx tan pronto como la carga útil se haya aceptado de forma duradera; procésala de forma asíncrona.

Orden

El orden de entrega se realiza según el mejor esfuerzo. En la práctica entregamos en el orden en que se emiten los eventos, pero los reintentos pueden cambiar el orden ante fallos. Siempre deduplica y concilia por call_id / id de objeto.

Duplicados

La entrega es al menos una vez: un reintento después de una respuesta que nunca vimos puede duplicar un evento. Cada reintento incluye el mismo event_id, así que almacena los ids procesados y omite las repeticiones. event_id también se comparte entre endpoints: dos endpoints suscritos al mismo evento reciben el mismo event_id.

Tiempos de espera

Las entregas a endpoints tienen un tiempo de espera de 30 s por intento. En la ruta heredada, las solicitudes bloqueantes que controlan el comportamiento de llamadas en vivo —el intercambio de configuración de telephony.incoming / web.incoming— agotan el tiempo de espera después de 10 s, pero una respuesta lenta retrasa la atención de la llamada, así que procura responder en un par de segundos. El despacho de herramientas en modo webhook permite 20 s de forma predeterminada, y las declaraciones de herramientas pueden establecer un timeout de nivel superior.

IPs de origen

Los webhooks salientes se originan desde el rango de IP en la nube de ThunderPhone. Si tu firewall requiere una lista de permitidos, contacta a soporte y te compartiremos los rangos actuales.

Elegir entre webhooks heredados y basados en endpoints

FunciónHeredado (/v1/webhook)Endpoints (/v1/developer/webhook-endpoints)
Cantidad de URL1 por organizaciónVarias por organización
Cobertura de eventosSolo telephony.* / web.*Los 10 tipos de eventos
Filtro de eventosPor endpoint
ReintentosNinguno8 intentos durante 24 h
Envoltoriotype + datatype + data + event_id
Rotación de secretosReemplaza el secreto únicoSecreto por endpoint
Desactivar sin eliminarstatus=disabled
Visibilidad de estadoactive / disabled / failing
Intercambio de configuración bloqueanteSí (telephony.incoming / web.incoming)Nunca: solo notificaciones
Ideal paraConfiguración dinámica de llamadasConsumo de eventos en producción

Las nuevas integraciones deben consumir eventos mediante webhooks basados en endpoints. Conserva (o agrega) una URL heredada solo si configuras llamadas dinámicamente al momento de atenderlas o usas el despacho de herramientas en modo webhook: esos intercambios de solicitud/respuesta solo se ejecutan en la ruta heredada.


Relacionado