Inicio / Sistema / Webhooks

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:

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.

Ver payload →

🎛 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:

IntentoEspera antesTotal acumulado
10 s
230 s30 s
32 min~2.5 min
410 min~13 min
51 h~1 h 13 min
66 h~7 h
724 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:

  1. Local con ngrok: ejecuta tu webhook en localhost (ej: puerto 3000), abre un túnel con ngrok http 3000 y configura la URL pública que ngrok te da en el panel de Wis.Chat.
  2. Envía un mensaje desde tu WhatsApp al número de la línea. Debería llegarte un evento message.received en segundos.
  3. Revisa el log: el archivo wischat_webhook.log debe tener el JSON crudo del evento.
  4. Envía un mensaje vía API a tu propio número y verifica que recibas eventos message.status con sent, delivered y eventualmente read.

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.