Webhooks
Recibe eventos de mensajes y sesiones en tu sistema, con firma HMAC.
Registra una URL tuya y te enviaremos un POST con cada evento que te interese.
Administrar webhooks
GET /webhooks # listar
POST /webhooks # crear
PUT /webhooks/{id} # actualizar (url, events, active)
DELETE /webhooks/{id} # eliminar
POST /webhooks
{
"url": "https://tu-sistema.com/webhooks/wsperu",
"events": ["message.status", "message.received", "session.disconnected"],
"sessionId": "665f..."
}
Al crear, la respuesta incluye un secret — guárdalo para verificar la firma.
Alcance por sesión (opcional)
sessionId limita el webhook a una línea:
- Omitido /
null→ el webhook se dispara para todas las sesiones de la cuenta. - Con
sessionId→ solo se dispara para esa línea.
Así podés tener un webhook global y/o webhooks separados por línea (ej: cobranza → sistema A, ventas → sistema B).
Eventos
| Evento | Cuándo se dispara |
|---|---|
message.sent |
El mensaje salió hacia WhatsApp |
message.status |
Cambió el estado de entrega (delivered, read) |
message.failed |
El envío falló tras los reintentos |
message.received |
Tu línea recibió un mensaje entrante |
session.connected |
La línea se conectó |
session.disconnected |
La línea se desconectó |
session.qr |
Se generó un QR nuevo (útil para re-vincular) |
Todos los eventos incluyen sessionId y sessionName al nivel raíz, así siempre identificás de qué línea proviene el evento (por id y por el nombre que le pusiste).
Ejemplo de payload
{
"event": "message.received",
"sessionId": "665f...",
"sessionName": "COBRANZA",
"data": {
"from": "51987654321@s.whatsapp.net",
"number": "51987654321",
"pushName": "Juan Pérez",
"messageId": "3EB0...",
"timestamp": 1755011111,
"type": "conversation",
"text": "Hola, quisiera información sobre mi pedido"
}
}
Campos de data en message.received:
| Campo | Descripción |
|---|---|
from |
JID del chat (puede ser @lid si el remitente oculta su número) |
number |
Número real del remitente (resuelto aunque el from sea @lid) |
pushName |
Nombre público que el remitente tiene en WhatsApp |
messageId |
Id del mensaje en WhatsApp |
timestamp |
Época Unix (segundos) |
type |
Tipo de mensaje (ver tabla abajo) |
text |
Cuerpo del mensaje, o el caption si es imagen/video/documento; null para media sin caption. En una reacción, es el emoji |
media |
Presente solo si el mensaje trae un archivo (ver siguiente sección) |
Valores de type (mensajes entrantes)
conversation (texto) · extendedTextMessage (texto con formato/link) · imageMessage · videoMessage · audioMessage · documentMessage · stickerMessage · reactionMessage · locationMessage · liveLocationMessage · contactMessage · contactsArrayMessage · pollCreationMessage · buttonsResponseMessage · listResponseMessage. Cualquier otro tipo llega como unknown.
Media entrante (imágenes, audio, video, documentos)
Cuando tu línea recibe un archivo, lo descargamos de WhatsApp, lo guardamos y adjuntamos un objeto media en data con una URL de descarga:
{
"event": "message.received",
"sessionId": "665f...",
"sessionName": "COBRANZA",
"data": {
"from": "51987654321@s.whatsapp.net",
"number": "51987654321",
"pushName": "Juan Pérez",
"messageId": "3EB0...",
"timestamp": 1755011111,
"type": "imageMessage",
"text": "el caption, si lo hubo",
"media": {
"url": "https://.../received/.../3EB0..._foto.jpg?...",
"mimetype": "image/jpeg",
"size": 216612,
"filename": "foto.jpg"
}
}
}
Campo de media |
Descripción |
|---|---|
url |
URL de descarga directa del archivo (válida por 24 horas) |
mimetype |
Tipo de contenido (ej: image/jpeg, audio/ogg, application/pdf) |
size |
Tamaño en bytes |
filename |
Nombre del archivo (el original en documentos; generado en el resto) |
Descargá el archivo apenas recibas el webhook y guardalo de tu lado — la url expira a las 24 horas. Se cubren imagen, video, audio, sticker y documento.
Cada request incluye el header x-webhook-signature: el HMAC-SHA256 del body usando tu secret.
const crypto = require('crypto');
function esValido(body, firma, secret) {
const esperada = crypto.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(esperada));
}
Reintentos
Si tu endpoint no responde 2xx, reintentamos hasta 3 veces con backoff exponencial. Responde rápido (encola internamente si tu procesamiento es lento) — el timeout es de 10 segundos.