Qué son
Un webhook es una URL tuya a la que enviamos un POST cada vez que ocurre un evento que has elegido.
Tú eliges qué
Cada endpoint se suscribe solo a los eventos que le interesan.
Firmado
Cada envío HTTP lleva una firma HMAC-SHA256 para que puedas verificar que viene de nosotros.
Con reintentos
Si tu endpoint falla, reintentamos con espera creciente hasta 8 veces.
Requisitos del endpoint
La URL debe usar HTTPS y resolver a una IP pública. No seguimos redirecciones: un 3xx se trata como fallo definitivo. Responde con un 2xx en cuanto recibas el evento y procesa después: cortamos a los 10 segundos.
Dar de alta un endpoint
Se gestiona desde el dashboard, en Integraciones. Solo el owner del workspace puede hacerlo.
- 1
Abre Integraciones → Webhooks y pulsa «Añadir webhook».
- 2
Elige el tipo de destino: HTTP (JSON firmado) o Discord.
- 3
Pega la URL y selecciona los eventos que quieres recibir.
- 4
Guarda y copia el secreto de firma: lo necesitarás para verificar los envíos.
- 5
Pulsa «Probar» para recibir un evento ping y confirmar que todo llega.
Catálogo de eventos
Los mensajes están separados por canal: message.* es solo SMS y whatsapp.* es solo WhatsApp, para que puedas suscribirte a uno sin recibir el otro.
Llamadas
call.started | Una llamada empieza a sonar. |
call.answered | Alguien descuelga la llamada. |
call.completed | La llamada termina con normalidad. |
call.missed | Nadie contestó la llamada (perdida, ocupado o sin respuesta). |
call.recording_ready | La grabación se ha archivado y su URL definitiva ya es estable. |
Mensajería
message.received | Entra un SMS. |
message.sent | Se acepta un SMS para envío. |
whatsapp.received | Entra un mensaje de WhatsApp. |
whatsapp.sent | Se acepta un mensaje de WhatsApp para envío. |
conversation.assigned | Una conversación se asigna a un agente distinto. |
Agentes y colas
agent.status_changed | Cambia la disponibilidad, el estado de routing o la conexión de un agente. |
queue.call_waiting | Una llamada entra en cola. |
queue.call_connected | Una llamada en cola se conecta con un agente. |
queue.call_abandoned | El llamante cuelga antes de ser atendido. |
Contactos y facturación
contact.created | Se crea un contacto nuevo. |
invoice.issued | Se emite una factura del periodo cerrado. |
Payload y cabeceras
Todos los endpoints HTTP reciben el mismo sobre versionado. Los datos propios de cada evento van dentro de data.
{
"id": "8f14e45f-ceea-467a-9f6e-4f2c1e0b3a77",
"eventId": "b1c2d3e4-5f60-4a7b-8c9d-0e1f2a3b4c5d",
"event": "call.completed",
"version": 1,
"createdAt": "2026-08-24T10:00:00.000Z",
"workspaceId": "2c8a1f6e-7b90-4d3a-8e5f-1a2b3c4d5e6f",
"data": {
"callId": "a3d9c1b2-4e5f-4a6b-9c8d-7e6f5a4b3c2d",
"callSid": "CA1234567890abcdef1234567890abcd",
"direction": "inbound",
"status": "completed",
"from": "+34600000000",
"to": "+34911223344",
"durationSeconds": 128
}
}| Cabecera | Significado |
|---|---|
x-callspace-event | Nombre del evento, p. ej. call.completed. |
x-callspace-event-id | Id del hecho de negocio, común a todos tus endpoints. |
x-callspace-delivery-id | Id de esta entrega concreta. |
idempotency-key | Igual al delivery id. Estable entre reintentos: úsalo para deduplicar. |
x-callspace-attempt | Número de intento, empezando en 1. |
x-callspace-timestamp | Segundos Unix en los que se firmó el envío. |
x-callspace-signature | t=<timestamp>,v1=<hmac-sha256 en hex>. |
Entrega al menos una vez
Preferimos repetir un evento antes que perderlo, así que tu endpoint puede recibir el mismo envío más de una vez (por ejemplo, si respondes tarde y el reintento ya salió). Deduplica por idempotency-key, que es constante a lo largo de todos los intentos de una misma entrega.
Verificar la firma
Comprueba siempre la firma antes de actuar sobre un evento: es lo único que distingue un envío nuestro de uno de cualquiera que conozca tu URL.
import crypto from 'node:crypto';
// El HMAC cubre "<timestamp>.<cuerpo crudo>". Necesitas el BODY SIN PARSEAR:
// si tu framework ya lo convirtió a objeto, vuelve a serializarlo y la firma
// dejará de cuadrar.
export function verifyCallspaceWebhook(rawBody, headers, secret) {
const header = headers['x-callspace-signature'] ?? '';
const parts = Object.fromEntries(
header.split(',').map((part) => part.trim().split('=')),
);
const timestamp = Number.parseInt(parts.t, 10);
if (!Number.isFinite(timestamp)) return false;
// Rechaza reenvíos antiguos.
const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
if (age > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Comparación en tiempo constante: un === filtraría el secreto por timing.
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(parts.v1 ?? '', 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Reintentos y auto-desactivación
Qué pasa cuando tu endpoint no responde como esperamos.
Reintentamos
Ante errores de red, timeouts y respuestas 5xx, 408 o 429. La espera crece: ~10s, 30s, 90s, 4.5m, 13.5m, 40m, 2h, hasta un tope de 6h, con un margen aleatorio para no saturarte cuando te recuperes.
No reintentamos
Ante un 4xx que no sea 408 o 429, y ante un 3xx. Son errores de configuración que no se arreglan repitiendo el envío.
Auto-desactivación
Tras 20 fallos definitivos seguidos, el endpoint se desactiva y deja de recibir eventos. Lo verás marcado en el dashboard y podrás reactivarlo desde ahí una vez arreglado. Cualquier entrega correcta pone el contador a cero.
Cada envío queda registrado en el historial de entregas del endpoint, con el código HTTP, el número de intento y el payload exacto. Desde ahí puedes reenviar manualmente cualquier entrega.
Destino Discord
Si lo que quieres es ver los eventos en un canal, elige el tipo Discord y te los formateamos como embeds legibles.
En Discord, crea un webhook desde Ajustes del canal → Integraciones → Webhooks y pega aquí la URL que te da.
Estos destinos no llevan firma HMAC: en Discord la propia URL es la credencial, así que trátala como un secreto. Si se filtra, bórrala en Discord y crea otra.