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

EventoCuándo se dispara
message.newLlega un mensaje entrante o se envía uno saliente confirmado por Meta. Trae el mensaje completo.
message.statusCambio de estado de un mensaje saliente (sent → delivered → read, o failed). Trae messageId y phoneNumberId.
conversation.updatedCambia el estado de una conversación (abierta, cerrada, reasignada, etc.).
conversation.handoff_requestedUna 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.unansweredUn 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.statusCambio de estado de plantilla (PENDING → APPROVED/REJECTED) por webhook de Meta. Trae wabaId.
quality.changedCambio 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.