Webhooks salientes
Mosend dispara webhooks HTTP POST hacia tu URL cada vez que ocurre un evento relevante. Los gestionas desde el dashboard (Configuración → Integraciones → Webhooks) o vía API.
Firma HMAC
Cada request lleva el header X-Mosend-Signature con HMAC SHA-256 del body crudo, usando el secreto que recibiste al crear el webhook. Tu endpoint debe validarlo antes de procesar el evento.
# Node.js — validación del HMAC
import crypto from 'node:crypto';
function verify(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}Estructura del payload
El cuerpo es JSON con type (el evento), organizationId, data (el recurso) y timestamp. El tipo de evento también viaja en el header X-Mosend-Event, y la firma en X-Mosend-Signature.
POST tu-url
X-Mosend-Event: message.new
X-Mosend-Signature: sha256=<hmac-del-body-crudo>
Content-Type: application/json
{
"type": "message.new",
"organizationId": "a1b2c3d4-...",
"data": {
"conversationId": "...",
"message": { "id": "...", "direction": "IN", "type": "text", "payload": {...}, ... }
},
"timestamp": "2026-05-01T03:42:18.123Z"
}Tu endpoint debe responder 2xx en menos de 10 s. Si falla, Mosend reintenta con backoff exponencial hasta 8 veces (≈30 minutos). Después marca el delivery como FAILED y queda en el log.
Eventos disponibles
| Evento | Cuándo se dispara |
|---|---|
| message.new | Llega un mensaje entrante o se envía uno saliente confirmado por Meta. Trae el mensaje completo. |
| message.status | Cambio de estado de un mensaje saliente (sent → delivered → read, o failed). Trae messageId y phoneNumberId. |
| conversation.updated | Cambia el estado de una conversación (abierta, cerrada, reasignada, etc.). |
| conversation.handoff_requested | Una conversación pide pasar a asesor humano (keyword, decisión del bot IA, regla o flujo). Solo el primer trigger — los recordatorios internos no re-pingean. |
| conversation.unanswered | Un mensaje del cliente lleva N minutos sin respuesta humana (umbral configurable, default 2 min). Bot IA / auto-respuestas / templates NO cuentan como respondido. Una sola notificación por mensaje, solo en conversaciones OPEN. |
| template.status | Cambio de estado de plantilla (PENDING → APPROVED/REJECTED) por webhook de Meta. Trae wabaId. |
| quality.changed | Cambio del quality rating del número (GREEN / YELLOW / RED). |
Restringir por número (phoneNumberIds)
Por defecto un webhook recibe eventos de todos los números de la org. Si tu integración solo debe enterarse de un número (ej. un bot externo que opera un único WhatsApp), pasá phoneNumberIds al crear o editar el webhook con los UUID de Mosend de los números permitidos. Vacío o ausente = todos.
# Crear un webhook acotado a un solo número
curl -X POST 'https://api.mosend.dev/organizations/{orgId}/webhooks-outbound' \
-H 'X-Api-Key: mk_live_<prefix>.<secret>' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://tu-app.com/webhooks/mosend",
"events": ["message.new", "message.status", "conversation.handoff_requested"],
"phoneNumberIds": ["<UUID-del-numero>"]
}'Cómo decide Mosend si entregar a un webhook con scope:
- El payload trae
phoneNumberId(la mayoría:message.*,conversation.*) → entrega solo si está en la lista permitida. - No trae número pero sí
wabaId(template.status) → entrega si la WABA tiene al menos un número permitido. - Eventos globales de la org sin número ni WABA (facturación, uso, etc.) → no se entregan a webhooks con scope. Para recibirlos, dejá un webhook sin
phoneNumberIds.
Umbral de conversation.unanswered
Si te suscribís a conversation.unanswered, podés ajustar cuántos minutos espera Mosend antes de avisarte con unansweredThresholdMinutes (1–1440, default 2). Solo cuenta como "respondido" un mensaje de un asesor humano — el bot IA, las auto-respuestas y las plantillas no cierran el reloj. Se emite una sola notificación por mensaje sin responder, y solo para conversaciones OPEN.
Idempotencia
Mosend reintenta cualquier entrega que no reciba un 2xx, así que tu endpoint puede recibir el mismo evento más de una vez. El payload no incluye un id de entrega: deduplicá por el id del recurso dentro de data — por ejemplo data.message.id para message.new / message.status, o data.conversationId para los eventos de conversación.