Guías del producto
Webhooks de mensajes entrantes (oyentes)
Un oyente (listener) conecta uno de tus números de WhatsApp con Unibox y puede enviar un webhook de mensajes entrantes a tu servidor. En el panel, los oyentes se gestionan en la página Disparadores Entrantes. Cuando un número es oyente, sus chats se sincronizan con la bandeja de entrada de Unibox y, si agregas una URL de webhook, Wbiztool envía cada mensaje nuevo a tu servidor. Usa los webhooks para registrar conversaciones en un CRM, avisar a tu equipo o crear una respuesta automática.
Antes de empezar#
- El complemento Unibox. Cuesta $20/mes o $200/año por número de WhatsApp, y la cantidad de oyentes que puedes tener es igual a la cantidad del complemento. Cómpralo en Complementos Disponibles de Facturación y Planes dentro del panel. Sin él, la página Disparadores Entrantes se abre igualmente, pero al hacer clic en Agregar Nuevo Oyente aparece Unibox Add-on Required (se requiere el complemento Unibox) con un botón Subscribe to Unibox Add-on (suscribirse al complemento Unibox).
- Un número de WhatsApp conectado en la configuración de WhatsApp. Solo se pueden agregar números conectados que todavía no sean oyentes.
- Debes ser propietario o editor del espacio de trabajo.
- Para los webhooks: una URL pública (usa
https) de 100 caracteres o menos que acepte solicitudesPOSTcon un cuerpo JSON.
Agregar un oyente#
Abre Disparadores Entrantes
En la barra lateral, abre Unibox y haz clic en Disparadores Entrantes, o ve a Disparadores Entrantes.
Empieza un nuevo oyente
Haz clic en la tarjeta Agregar Nuevo Oyente.
Elige el número
Elígelo en Select WhatsApp Number (seleccionar número de WhatsApp). Si la lista dice No available WhatsApp numbers (no hay números de WhatsApp disponibles), todos los números conectados ya son oyentes o no hay ninguno conectado.
Agrega un webhook (opcional)
Ingresa tu URL del Webhook (Opcional). Cuando escribes una URL, aparece Webhook Events (eventos del webhook): deja marcado Incoming Messages (mensajes entrantes), Outgoing Messages (mensajes salientes) o ambos. Puedes agregar o cambiar la URL más tarde.
Guarda
Haz clic en Agregar Oyente. El oyente aparece como una tarjeta con el estado Active (activo). Si ingresaste una URL, se crea un secreto de webhook para ella.
Gestionar oyentes#
Cada tarjeta muestra el número, su estado, la URL del Webhook: (o No configurado), la Última Actividad: (cuándo se revisó por última vez si el número tenía mensajes, o Nunca) y el Secreto del Webhook:, oculto hasta que haces clic en el botón del ojo.
Abre el menú ⋮ de una tarjeta para:
| Acción | Qué ocurre |
|---|---|
| Editar | Cambia la URL del webhook, los eventos del webhook o el secreto. El número no se puede cambiar. |
| Deshabilitar / Habilitar | Al deshabilitarlo, el oyente pasa a Inactive (inactivo): el número deja de sincronizarse con la bandeja de entrada y no se envían webhooks. Al habilitarlo vuelve a estar Active. |
| Eliminar | Quita el oyente después de que confirmes. Las conversaciones que ya están en la bandeja de entrada se conservan. Si más adelante vuelves a agregar el mismo número, se restaura el oyente. Si ingresas una URL de webhook al volver a agregarlo, el oyente conserva su secreto de webhook anterior, si tenía uno, en lugar de recibir uno nuevo. |
Estados del oyente#
| Estado | Significado |
|---|---|
| Active (activo) | Los mensajes se sincronizan y se envían webhooks mientras el número está conectado. |
| Pending (pendiente) | El número no estaba conectado cuando se creó el oyente, por ejemplo desde Zapier. Haz clic en Habilitar cuando el número esté conectado. |
| Inactive (inactivo) | Deshabilitado. No se sincroniza nada y no se envían webhooks. |
Estadísticas#
| Tarjeta | Qué muestra |
|---|---|
| Oyentes Activos | Todos los oyentes de la página, incluidos los deshabilitados. |
| Números Disponibles | Los números conectados que todavía no son oyentes. Los números cuyo oyente eliminaste siguen contando como usados aquí, así que puede mostrar menos números de los que realmente puedes agregar. |
| Límite Total | Cuántos oyentes permite tu complemento Unibox. |
| Mensajes Hoy | Todavía no se contabiliza; siempre muestra 0. |
Cambiar la URL o los eventos del webhook#
Abre el oyente
Haz clic en ⋮ en la tarjeta y luego en Editar.
Actualiza la configuración
Cambia la Webhook URL (Optional) (URL del webhook, opcional) y los Webhook Events. Borra la URL para dejar de enviar webhooks de este número: también se elimina su secreto, y se crea uno nuevo si vuelves a agregar una URL.
Guarda
Haz clic en Update Listener (actualizar oyente).
Regenerar el secreto#
En Edit Listener (editar oyente), haz clic en el botón de actualizar junto a Webhook Secret (secreto del webhook) y confirma. El nuevo secreto se guarda de inmediato, aunque después cierres el cuadro de diálogo sin hacer clic en Update Listener, y a partir de ese momento las solicitudes se firman con él. Actualiza tu servidor con el nuevo secreto de inmediato.
Cómo se entregan los webhooks#
Wbiztool envía una solicitud POST a tu URL por cada mensaje nuevo encontrado cuando se sincroniza el número, algo que ocurre cada pocos minutos mientras el número está conectado y no está ocupado enviando mensajes.
- Eventos:
message_receivedpara los mensajes que te envían a tu número, ymessage_sentpara los mensajes enviados desde él (desde el teléfono, campañas o la API). Solo se envían los eventos marcados en Webhook Events. - Las respuestas desde la bandeja de entrada de Unibox normalmente no generan
message_sent, porque la bandeja de entrada ya las tiene cuando se ejecuta la sincronización. - Respuesta: responde con HTTP
200en menos de 8 segundos. Cualquier otra respuesta o un tiempo de espera agotado cuenta como entrega fallida. - Sin reintentos: cada mensaje se envía una sola vez. Si tu servidor está caído, ese webhook se pierde.
- Orden: las solicitudes se envían de forma independiente y pueden llegar desordenadas. Ordena por
message.timestampsi el orden importa.
Encabezados#
| Encabezado | Valor |
|---|---|
Content-Type | application/json |
X-Wbiztool-Event | message_received o message_sent |
X-Wbiztool-Timestamp | Cuándo se envió el webhook, en ISO 8601 UTC. Igual que timestamp en el cuerpo. |
X-Wbiztool-Webhook-Id | El ID del oyente. Igual que webhook_id en el cuerpo. |
X-Wbiztool-Signature | sha256= seguido de la firma. Se envía siempre que el oyente tiene un secreto, lo que ocurre siempre que hay una URL configurada. |
Payload#
{
"event": "message_received",
"timestamp": "2026-09-16T10:31:12.482913+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0C1A2B3D4E5F60718",
"type": "chat",
"content": "Hi, is my order #4821 out for delivery?",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:29:58+00:00",
"whatsapp_timestamp": 1789554598,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_sent",
"timestamp": "2026-09-16T10:33:40.117205+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0F9E8D7C6B5A40312",
"type": "chat",
"content": "Yes, it will reach you today by 6 PM.",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:31:04+00:00",
"whatsapp_timestamp": 1789554664,
"direction": "outgoing",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_received",
"timestamp": "2026-09-16T10:36:02.904311+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected]_3EB0A7B6C5D4E3F20109",
"type": "image",
"content": "📸 Image",
"from": "919876543210",
"from_name": "Aman",
"to": "919812345678",
"timestamp": "2026-09-16T10:34:51+00:00",
"whatsapp_timestamp": 1789554891,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0,
"media": {
"filename": "",
"mimetype": "image/jpeg",
"size": 245760
}
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "919876543210",
"name": "Aman",
"is_group": false,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
}
}{
"event": "message_received",
"timestamp": "2026-09-16T10:40:15.330187+00:00",
"webhook_id": 42,
"whatsapp_client_id": "678",
"whatsapp_phone": "919812345678",
"message": {
"id": "[email protected][email protected]",
"type": "chat",
"content": "Is the store open on Sunday?",
"from": "120363041234567890",
"from_name": "Acme Loyalty Club",
"to": "919812345678",
"timestamp": "2026-09-16T10:39:02+00:00",
"whatsapp_timestamp": 1789555142,
"direction": "incoming",
"status": "pending",
"is_forwarded": false,
"forwarding_score": 0
},
"contact": {
"whatsapp_id": "[email protected]",
"phone": "120363041234567890",
"name": "Acme Loyalty Club",
"is_group": true,
"is_business": false
},
"organisation": {
"id": "10314",
"name": "Acme Stores"
},
"group": {
"name": "Acme Loyalty Club"
}
}Los números, ID y nombres anteriores son ejemplos.
Campos de primer nivel#
| Campo | Tipo | Descripción |
|---|---|---|
event | string | message_received o message_sent. |
timestamp | string | Cuándo se envió el webhook (ISO 8601, UTC). |
webhook_id | integer | El ID del oyente. |
whatsapp_client_id | string | El ID de tu número de WhatsApp, tal como aparece en la configuración de WhatsApp. |
whatsapp_phone | string | Tu número de WhatsApp. |
message | object | El mensaje. Consulta más abajo. |
contact | object | La persona o el grupo con quien es la conversación. Consulta más abajo. |
organisation | object | id (string) y name de tu espacio de trabajo. |
group | object | Solo en chats de grupo: name, el nombre del grupo. |
Campos de message#
| Campo | Tipo | Descripción |
|---|---|---|
id | string | El ID de WhatsApp del mensaje. Úsalo para ignorar duplicados. |
type | string | chat para texto. En los demás casos, el tipo de WhatsApp, como image, video, audio, ptt (nota de voz), document, sticker o location. |
content | string | El texto en los mensajes chat. En los archivos multimedia, una etiqueta: 📸 Image, 🎥 Video, 🎵 Audio, 🎤 Voice Message, 😊 Sticker, Document: seguido del texto del documento (Document cuando no hay texto), o el nombre del tipo con la inicial en mayúscula en cualquier otro caso, como Location. No se incluyen los pies de foto. |
from | string | Siempre el número del contacto (o el ID del grupo), en ambas direcciones. |
from_name | string | El nombre del contacto o del grupo. Puede estar vacío. |
to | string | Siempre tu número de WhatsApp, en ambas direcciones. |
timestamp | string | Cuándo se envió el mensaje en WhatsApp (ISO 8601, UTC). |
whatsapp_timestamp | integer | La misma hora como marca de tiempo Unix en segundos. |
direction | string | incoming o outgoing. Usa este campo, no from y to, para saber la dirección. |
status | string | Actualmente siempre es pending. No lo uses para saber el estado de entrega o de lectura. |
is_forwarded | boolean | Si el mensaje fue reenviado. |
forwarding_score | integer | Cuántas veces fue reenviado. |
media | object | En mensajes multimedia, cuando hay detalles disponibles: filename, mimetype y size en bytes. El archivo en sí no se incluye. |
quoted_message_id | string | Solo cuando el mensaje responde a otro mensaje. |
Campos de contact#
| Campo | Tipo | Descripción |
|---|---|---|
whatsapp_id | string | El ID de WhatsApp, como [email protected] para una persona o …@g.us para un grupo. |
phone | string | El número sin +, o el ID del grupo en el caso de los grupos. |
name | string | El nombre que Wbiztool tiene para el contacto, o el nombre del grupo. Puede estar vacío. |
is_group | boolean | true en los chats de grupo. |
is_business | boolean | true en las cuentas de WhatsApp Business, cuando se sabe. |
Verificar la firma#
Cada solicitud se firma con el secreto de tu oyente usando HMAC-SHA256. La firma se calcula sobre el cuerpo sin procesar de la solicitud, exactamente como se recibe, y se envía como sha256= más el resumen hexadecimal en minúsculas en X-Wbiztool-Signature.
Calcula siempre la firma a partir de los bytes sin procesar antes de analizar el JSON. Analizar y volver a codificar el cuerpo lo modifica (por ejemplo, los caracteres no ingleses y los emojis llegan escapados como \uXXXX), y la firma no coincidirá.
// Express: keep the raw body for this route
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WBIZTOOL_WEBHOOK_SECRET;
app.post("/wbiztool/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const received = req.get("X-Wbiztool-Signature") || "";
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send("Invalid signature");
const data = JSON.parse(req.body.toString("utf8"));
if (data.event === "message_received") {
console.log(`New message from ${data.contact.phone}: ${data.message.content}`);
}
res.sendStatus(200); // reply quickly; do slow work in the background
});
app.listen(3000);# Flask
import hashlib
import hmac
import json
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"].encode()
@app.post("/wbiztool/webhook")
def wbiztool_webhook():
raw_body = request.get_data() # raw bytes, before parsing
expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
received = request.headers.get("X-Wbiztool-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
data = json.loads(raw_body)
if data["event"] == "message_received":
print(f"New message from {data['contact']['phone']}: {data['message']['content']}")
return "", 200<?php
$secret = getenv('WBIZTOOL_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
$received = $_SERVER['HTTP_X_WBIZTOOL_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($rawBody, true);
if ($data['event'] === 'message_received') {
error_log('New message from ' . $data['contact']['phone'] . ': ' . $data['message']['content']);
}
http_response_code(200);La firma no cubre el encabezado de la marca de tiempo, así que no protege contra la repetición de una solicitud. Si eso te importa, guarda cada message.id que hayas procesado e ignora las repeticiones.
Solución de problemas#
| Mensaje o problema | Qué hacer |
|---|---|
| Unibox Add-on Required al hacer clic en Agregar Nuevo Oyente | Tu espacio de trabajo no tiene el complemento Unibox. Cómpralo en Complementos Disponibles de Facturación y Planes dentro del panel. |
You have reached your unibox numbers limit | Elimina un oyente que ya no necesites o aumenta la cantidad del complemento. |
| No available WhatsApp numbers | Conecta otro número, o ese número ya es un oyente. |
This WhatsApp number is already a listener | Edita la tarjeta existente. |
Invalid WhatsApp client | El número se desconectó. Vuelve a conectarlo en la configuración de WhatsApp y recarga la página. |
Error que menciona value too long al guardar | La URL del webhook tiene más de 100 caracteres. Usa una URL más corta. |
| No llega ningún webhook | Comprueba que el oyente esté Active, que el número esté conectado, que el tipo de evento esté marcado y que tu URL sea pública, con https y un certificado válido. Los mensajes solo se envían después de la siguiente sincronización, unos minutos más tarde. |
| Faltan algunos webhooks | Tu servidor devolvió algo distinto de 200, tardó más de 8 segundos o no estaba disponible. Las entregas fallidas no se reintentan. |
| La firma no coincide | Usa el cuerpo sin procesar, no JSON recodificado, y el secreto actual. Regenerar el secreto, o conectar Zapier, lo reemplaza. |
| Última Actividad: dice Nunca | El número todavía no se ha revisado. Debe estar conectado y no estar ocupado enviando mensajes. |
