Webhooks
Un webhook es una URL pública en tu servidor que Wis.Chat invoca cada vez que ocurre un evento: mensajes entrantes de clientes, cambios de estado de tus envíos, respuestas a botones, lecturas, errores. Esta página explica cómo construirlo y entrega ejemplos descargables en PHP, Node.js, Python y C#.
¿Qué es un webhook?
A diferencia de la API de envío (donde tú llamas a Wis.Chat), el webhook funciona al revés:
Wis.Chat te llama a ti. Cada vez que algo pasa con tu línea (un mensaje entrante,
un mensaje que se entregó, un botón que el cliente tocó), Wis.Chat hace un POST HTTP
a la URL que tú configures, con el evento en formato JSON.
┌──────────────┐ 1) Cliente escribe ┌──────────────────────┐
│ WhatsApp │ ─────────────────────────► │ api.wis.chat │
│ del cliente │ o cambia estado │ (recibe de Meta) │
└──────────────┘ └──────────┬───────────┘
│
│ 2) POST JSON al webhook
▼
┌──────────────────────┐
│ TU SERVIDOR │
│ /webhook.php │
│ (responde 200 OK) │
└──────────────────────┘
Configurar tu URL de webhook
Desde el panel wis.chat/wischat → Líneas → tu número → Webhook configura la URL pública donde tu aplicación escuchará los eventos. La URL debe:
- Ser HTTPS con un certificado válido (no self-signed).
- Responder HTTP 200 en menos de 5 segundos. Si tarda más, Wis.Chat reintenta.
- Ser accesible desde Internet. Si estás desarrollando en localhost, usa ngrok o similar para exponer un túnel público.
Token de verificación: al configurar la URL, el panel te pide un
verify_token que tú eliges. Wis.Chat lo enviará en cada request en el header
X-Wischat-Token para que valides que la llamada viene del gateway y no de un
tercero malicioso.
Tipos de eventos que recibirás
📥 Mensaje entrante
El cliente te escribió: texto, imagen, audio, ubicación, documento, etc.
Ver payload →📊 Cambio de estado
Un mensaje que enviaste cambió de estado: sent, delivered, read, failed.
🎛 Botón / Lista
El cliente tocó un botón o seleccionó una opción de una lista interactiva que enviaste.
Ver payload →⚠ Error de envío
Un mensaje no pudo entregarse al cliente final (número inválido, bloqueado, etc.).
Ver payload →Payload: mensaje entrante de texto
{
"event": "message.received",
"tid": 15,
"timestamp": "2026-05-07T14:23:11Z",
"data": {
"wamid": "wamid.HBgMNTkz...",
"from": "593911223344",
"to": "593987654321",
"type": "text",
"text": {
"body": "Hola, quisiera información sobre los planes"
},
"contact": {
"profile_name": "Oscar Toapanta"
}
}
}Payload: cambio de estado
{
"event": "message.status",
"tid": 15,
"timestamp": "2026-05-07T14:23:13Z",
"data": {
"wamid": "wamid.HBgMNTkz...",
"recipient": "593911223344",
"status": "delivered",
// "sent" | "delivered" | "read" | "failed"
"timestamps": {
"sent": "2026-05-07T14:23:11Z",
"delivered": "2026-05-07T14:23:13Z",
"read": null,
"failed": null
}
}
}Payload: respuesta a botón o lista
{
"event": "message.received",
"tid": 15,
"timestamp": "2026-05-07T14:25:42Z",
"data": {
"wamid": "wamid.HBgMNTkz...",
"from": "593911223344",
"type": "interactive",
"interactive": {
"type": "button_reply",
// o "list_reply"
"button_reply": {
"id": "btn_factura",
"title": "📄 Factura"
}
}
}
}Payload: error de envío
{
"event": "message.status",
"tid": 15,
"timestamp": "2026-05-07T14:23:14Z",
"data": {
"wamid": "wamid.HBgMNTkz...",
"status": "failed",
"error": {
"code": 131026,
"title": "Message undeliverable",
"message": "Receiver is incapable of receiving this message."
}
}
}Implementación de tu webhook
Estos ejemplos muestran un webhook funcional listo para producción. Solo cambia
TU_TOKEN_VERIFICACION por el valor que configuraste en el panel:
Archivo: webhook.php — sube este archivo a tu servidor (apunta tu URL pública a este script):
<?php // webhook.php — Wis.Chat webhook receiver $VERIFY_TOKEN = 'TU_TOKEN_VERIFICACION'; $LOG_FILE = __DIR__ . '/wischat_webhook.log'; // 1. Validar token de verificación $tokenRecibido = $_SERVER['HTTP_X_WISCHAT_TOKEN'] ?? ''; if (!hash_equals($VERIFY_TOKEN, $tokenRecibido)) { http_response_code(401); echo 'Unauthorized'; exit; } // 2. Leer y decodificar el payload $body = file_get_contents('php://input'); $evt = json_decode($body, true); if (!$evt) { http_response_code(400); echo 'Bad JSON'; exit; } // 3. Loguear (siempre — útil para debug) file_put_contents($LOG_FILE, date('c') . ' ' . $body . "\n", FILE_APPEND ); // 4. Responder OK rápido — el procesamiento pesado va en background http_response_code(200); echo 'OK'; if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); // 5. Procesar el evento $tipo = $evt['event'] ?? 'unknown'; $data = $evt['data'] ?? []; switch ($tipo) { case 'message.received': manejarMensajeEntrante($data); break; case 'message.status': actualizarEstado($data); break; } function manejarMensajeEntrante($data) { $de = $data['from'] ?? ''; $tipo = $data['type'] ?? ''; if ($tipo === 'text') { $texto = $data['text']['body'] ?? ''; // TODO: guardar en tu BD, disparar un bot, notificar agente, etc. } elseif ($tipo === 'interactive') { $btnId = $data['interactive']['button_reply']['id'] ?? null; // TODO: rutear según el botón que tocó el cliente } } function actualizarEstado($data) { $wamid = $data['wamid'] ?? ''; $estado = $data['status'] ?? ''; // TODO: actualizar el estado del mensaje en tu BD }
Archivo: webhook.js — instala dependencias con npm i express y ejecuta con node webhook.js:
// webhook.js — Wis.Chat webhook receiver (Node.js + Express) const express = require('express'); const fs = require('fs'); const VERIFY_TOKEN = 'TU_TOKEN_VERIFICACION'; const PORT = 3000; const LOG_FILE = './wischat_webhook.log'; const app = express(); app.use(express.json({ limit: '1mb' })); app.post('/webhook', (req, res) => { // 1. Validar token const token = req.headers['x-wischat-token']; if (token !== VERIFY_TOKEN) return res.status(401).send('Unauthorized'); // 2. Loguear fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + JSON.stringify(req.body) + '\n' ); // 3. Responder OK rápido — procesa async después res.status(200).send('OK'); // 4. Procesar evento (sin bloquear la respuesta) setImmediate(() => procesarEvento(req.body)); }); function procesarEvento(evt) { const { event: tipo, data } = evt; if (tipo === 'message.received') { manejarMensajeEntrante(data); } else if (tipo === 'message.status') { actualizarEstado(data); } } function manejarMensajeEntrante(data) { const { from: de, type: tipo } = data; if (tipo === 'text') { const texto = data.text?.body; // TODO: guardar en BD, disparar bot, notificar agente, etc. } else if (tipo === 'interactive') { const btnId = data.interactive?.button_reply?.id; // TODO: rutear según el botón } } function actualizarEstado(data) { const { wamid, status: estado } = data; // TODO: actualizar estado del mensaje en tu BD } app.listen(PORT, () => console.log(`Webhook escuchando en :${PORT}`));
Archivo: webhook.py — instala dependencias con pip install flask y ejecuta con python webhook.py:
# webhook.py — Wis.Chat webhook receiver (Flask) from flask import Flask, request, jsonify from threading import Thread import json, datetime VERIFY_TOKEN = 'TU_TOKEN_VERIFICACION' LOG_FILE = 'wischat_webhook.log' app = Flask(__name__) @app.route('/webhook', methods=['POST']) def webhook(): # 1. Validar token if request.headers.get('X-Wischat-Token') != VERIFY_TOKEN: return 'Unauthorized', 401 evt = request.get_json(silent=True) if evt is None: return 'Bad JSON', 400 # 2. Loguear with open(LOG_FILE, 'a') as f: f.write(f'{datetime.datetime.utcnow().isoformat()} {json.dumps(evt)}\n') # 3. Procesar en background — responde rápido Thread(target=procesar_evento, args=(evt,)).start() return 'OK', 200 def procesar_evento(evt): tipo = evt.get('event') data = evt.get('data', {}) if tipo == 'message.received': manejar_mensaje_entrante(data) elif tipo == 'message.status': actualizar_estado(data) def manejar_mensaje_entrante(data): de = data.get('from') tipo = data.get('type') if tipo == 'text': texto = data.get('text', {}).get('body', '') # TODO: guardar en BD, disparar bot, notificar agente, etc. elif tipo == 'interactive': btn_id = data.get('interactive', {}).get('button_reply', {}).get('id') # TODO: rutear según el botón def actualizar_estado(data): wamid = data.get('wamid') estado = data.get('status') # TODO: actualizar estado del mensaje en tu BD if __name__ == '__main__': app.run(host='0.0.0.0', port=3000)
Archivo: WebhookController.cs — ASP.NET Core Web API. Crea un proyecto con dotnet new webapi y agrega este controlador:
// WebhookController.cs — Wis.Chat webhook receiver (ASP.NET Core) using Microsoft.AspNetCore.Mvc; using System.Text.Json; [ApiController] [Route("webhook")] public class WebhookController : ControllerBase { private const string VerifyToken = "TU_TOKEN_VERIFICACION"; private const string LogFile = "wischat_webhook.log"; [HttpPost] public async Task<IActionResult> Recibir() { // 1. Validar token if (!Request.Headers.TryGetValue("X-Wischat-Token", out var token) || token != VerifyToken) { return Unauthorized(); } // 2. Leer body using var reader = new StreamReader(Request.Body); var body = await reader.ReadToEndAsync(); var evt = JsonDocument.Parse(body).RootElement; // 3. Loguear await System.IO.File.AppendAllTextAsync(LogFile, $"{DateTime.UtcNow:o} {body}\n"); // 4. Procesar en background _ = Task.Run(() => ProcesarEvento(evt)); return Ok(); } private void ProcesarEvento(JsonElement evt) { var tipo = evt.GetProperty("event").GetString(); var data = evt.GetProperty("data"); switch (tipo) { case "message.received": ManejarMensajeEntrante(data); break; case "message.status": ActualizarEstado(data); break; } } private void ManejarMensajeEntrante(JsonElement data) { var de = data.GetProperty("from").GetString(); var tipo = data.GetProperty("type").GetString(); if (tipo == "text") { var texto = data.GetProperty("text").GetProperty("body").GetString(); // TODO: guardar en BD, disparar bot, notificar agente, etc. } else if (tipo == "interactive") { var btnId = data.GetProperty("interactive") .GetProperty("button_reply") .GetProperty("id").GetString(); // TODO: rutear según el botón } } private void ActualizarEstado(JsonElement data) { var wamid = data.GetProperty("wamid").GetString(); var estado = data.GetProperty("status").GetString(); // TODO: actualizar estado del mensaje en tu BD } }
Reintentos automáticos
Si tu webhook responde con un código distinto a 2xx, o si tarda más de
5 segundos en responder, Wis.Chat reintentará el envío con
backoff exponencial:
| Intento | Espera antes | Total acumulado |
|---|---|---|
| 1 | — | 0 s |
| 2 | 30 s | 30 s |
| 3 | 2 min | ~2.5 min |
| 4 | 10 min | ~13 min |
| 5 | 1 h | ~1 h 13 min |
| 6 | 6 h | ~7 h |
| 7 | 24 h | ~31 h |
Tras el último intento sin éxito, el evento se descarta. Por eso es importante que tu webhook
esté siempre disponible y responda 200 OK rápido — el procesamiento pesado
(guardar en BD, disparar lógica de negocio) hazlo después de responder.
Idempotencia
Como Wis.Chat puede reintentar un mismo evento varias veces, tu webhook debe ser idempotente: procesar dos veces el mismo evento no debe romper nada.
La forma más simple es deduplicar por wamid:
Antes de procesar un evento, busca en tu base de datos si ya tienes ese wamid
registrado. Si ya existe, ignora el evento. Si no existe, procésalo y guárdalo.
Seguridad
🔐 Valida el token
Siempre verifica el header X-Wischat-Token. Sin esa validación, cualquiera podría enviar payloads falsos a tu webhook.
🔒 HTTPS obligatorio
Tu webhook debe estar en HTTPS con certificado válido. HTTP plano expone los mensajes de tus clientes en tránsito.
⏱ Responde rápido
El procesamiento pesado va en background. Responder < 5s evita reintentos innecesarios y duplicados.
📝 Loguea todo
Especialmente al inicio, registra todos los eventos crudos. Facilita debugging cuando algo no funciona como esperas.
Probar tu webhook
Para verificar que tu webhook está bien antes de poner tráfico real:
- Local con ngrok: ejecuta tu webhook en localhost (ej: puerto 3000),
abre un túnel con
ngrok http 3000y configura la URL pública que ngrok te da en el panel de Wis.Chat. - Envía un mensaje desde tu WhatsApp al número de la línea. Debería
llegarte un evento
message.receiveden segundos. - Revisa el log: el archivo
wischat_webhook.logdebe tener el JSON crudo del evento. - Envía un mensaje vía API a tu propio número y verifica que recibas
eventos
message.statusconsent,deliveredy eventualmenteread.
Reenvío manual de eventos: desde el panel wis.chat/wischat → Líneas → Webhook → Historial puedes ver los últimos eventos enviados y reenviarlos manualmente para depurar.