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:
Varias URL, secretos por endpoint, filtros de eventos por endpoint
y reintentos automáticos.
Adminístralos mediante GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints.
Una URL por organización. Incluye los eventos del ciclo de vida de las llamadas,
incluidos los intercambios de configuración bloqueantes. Se administra mediante GET/PUT /v1/webhook.
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
- Lee el cuerpo sin procesar de la solicitud antes de realizar cualquier análisis.
- Calcula
hmac_sha256(secret, body).hexdigest(). - 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.
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 "", 204import 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ón | Heredado (/v1/webhook) | Endpoints (/v1/developer/webhook-endpoints) |
|---|---|---|
| Cantidad de URL | 1 por organización | Varias por organización |
| Cobertura de eventos | Solo telephony.* / web.* | Los 10 tipos de eventos |
| Filtro de eventos | — | Por endpoint |
| Reintentos | Ninguno | 8 intentos durante 24 h |
| Envoltorio | type + data | type + data + event_id |
| Rotación de secretos | Reemplaza el secreto único | Secreto por endpoint |
| Desactivar sin eliminar | — | status=disabled |
| Visibilidad de estado | — | active / disabled / failing |
| Intercambio de configuración bloqueante | Sí (telephony.incoming / web.incoming) | Nunca: solo notificaciones |
| Ideal para | Configuración dinámica de llamadas | Consumo 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
Todos los tipos de eventos y sus cargas útiles.
Administra varios endpoints, filtros de eventos y secretos.
La solicitud bloqueante que tu servidor debe responder para configurar llamadas.
Carga útil posterior a la llamada con transcripción, grabación y métricas.