¿Qué es un webhook?
Un webhook es una URL de tu servidor que whapi.co llama con POST cuando ocurre un evento en tu número de WhatsApp. Tú recibes un JSON y respondes 200 OK.
/api/test-webhook.
          1) Configurar la URL de webhook
- Ve a Dashboard → abre tu instancia (p. ej. /manage_instance.php?id=123).
- En Webhook URL escribe la dirección de tu endpoint (p. ej. https://tu-dominio.com/webhook) y guarda.
- Usa Test Webhook para enviar un evento de prueba (test_webhook).
2) Eventos que envía whapi.co
a) message_received (entrante)
          Se emite cuando recibes un mensaje. Si trae multimedia, media_url apunta al archivo temporal en /uploads/whatsapp/.
| Campo | Tipo | Descripción | 
|---|---|---|
| event_type | string | Siempre message_received | 
| id | string | ID del mensaje | 
| instance_id | string|number | Instancia | 
| chat_id | string | JID del chat ( @c.us/@g.us) | 
| from | string | Remitente real (en grupos puede cambiar) | 
| to | string | Destinatario | 
| from_me | 0|1 | Entrantes = 0 | 
| sender_name | string|null | Display name | 
| body | string | Texto (vacío en media) | 
| type | string | text,image,video,audio,document,sticker,location,vcard | 
| timestamp | number | Unix epoch (s) | 
| is_group | 0|1 | Es grupo | 
| quoted_message_id | string|null | Si citó otro mensaje | 
| reaction | string|null | Emoji si aplica | 
| media_url | string|null | URL pública temporal | 
| latitude/longitude | number|null | Si type=location | 
| vcard | string|null | vCard bruta | 
| status | string | Siempre received | 
| ack | string | pendingal ingresar | 
| group_sender | string|null | Remitente en grupo | 
{ 
  "event_type":"message_received",
  "id":"ABCD1234",
  "instance_id":"123",
  "chat_id":"573001112233@c.us",
  "from":"573001112233@c.us",
  "to":"573009998877@c.us",
  "from_me":0,
  "sender_name":"Juan Pérez",
  "body":"Hola, ¿tienen menú?",
  "type":"text",
  "timestamp":1724716800,
  "is_group":0,
  "quoted_message_id":null,
  "reaction":null,
  "media_url":null,
  "latitude":null,
  "longitude":null,
  "vcard":null,
  "status":"received",
  "ack":"pending",
  "group_sender":null
}
              b) message_sent (saliente)
          Se emite cuando tu instancia envía un mensaje (desde /api/send-message o manualmente).
{
  "event_type":"message_sent",
  "id":"WXYZ5678",
  "instance_id":"123",
  "chat_id":"573001112233@c.us",
  "from":"573009998877@c.us",
  "to":"573001112233@c.us",
  "body":"¡Hola! Te comparto el menú 👇",
  "type":"text",
  "timestamp":1724716890,
  "is_group":0,
  "media_url":null,
  "latitude":null,
  "longitude":null,
  "vcard":null,
  "status":"sent",
  "group_sender":null
}
          c) ack_update
          Actualiza el estado del mensaje: pending → sent → delivered → read.
{
  "event_type":"ack_update",
  "id":"WXYZ5678",
  "ack":"delivered"
}
          d) message_reaction
          {
  "event_type":"message_reaction",
  "instance_id":"123",
  "message_id":"WXYZ5678",
  "from_number":"573001112233@c.us",
  "reaction":"👍",
  "timestamp":1724717000
}
          e) test_webhook
          {
  "event_type":"test_webhook",
  "instance_id":"123",
  "timestamp":1724717100,
  "message":"This is a test webhook message from WhApi.co"
}
          3) Endpoint de configuración (API whapi.co)
Además del panel, puedes actualizar el webhook por API:
POST /api/instance/:id/webhook
Content-Type: application/json
{
  "token": "TOKEN_DE_LA_INSTANCIA",
  "url": "https://tu-dominio.com/webhook",
  "secret": "opcional_para_hmac"
}
          Si defines secret, tu servidor recibirá la cabecera X-Whapi-Signature (HMAC-SHA256 sobre el cuerpo) para validar integridad/autenticidad.
4) Tu endpoint receptor
Ejemplos listos para pegar. Incluyen idempotencia y validación HMAC (X-Whapi-Signature).
const express = require("express");
const crypto = require("crypto");
const app = express();
// IMPORTANTE: conservar el raw body para HMAC
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
const SEEN = new Set();            // usa Redis en producción
const WEBHOOK_SECRET = process.env.WHAPI_WEBHOOK_SECRET || "";
function verifyHmac(req) {
  if (!WEBHOOK_SECRET) return true; // si no configuraste secret, no hay firma
  const sig = req.get("X-Whapi-Signature") || "";
  const h = crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.rawBody || "").digest("hex");
  return crypto.timingSafeEqual(Buffer.from(h), Buffer.from(sig));
}
app.post("/webhook", async (req, res) => {
  try {
    if (!verifyHmac(req)) return res.sendStatus(401);
    const ev = req.body || {};
    const key = ev.id || ev.message_id || (ev.event_type + ":" + ev.timestamp);
    if (key) {
      if (SEEN.has(key)) return res.sendStatus(200);
      SEEN.add(key);
    }
    switch (ev.event_type) {
      case "message_received":
        console.log("📥 In:", ev.from, "->", ev.to, "|", ev.type, "|", ev.body);
        // TODO: guardar en BD, invocar bot, etc.
        break;
      case "message_sent":
        console.log("📤 Out:", ev.to, "|", ev.type, "|", ev.status);
        break;
      case "ack_update":
        console.log("✅ ACK:", ev.id, "->", ev.ack);
        break;
      case "message_reaction":
        console.log("😍 Reaction:", ev.message_id, ev.reaction, "by", ev.from_number);
        break;
      case "test_webhook":
        console.log("🧪 Test:", ev.message);
        break;
      default:
        console.log("ℹ️ Evento desconocido:", ev.event_type);
    }
    res.sendStatus(200);
  } catch (e) {
    console.error("Webhook error:", e);
    res.sendStatus(200); // ya recibimos el evento; procesa asíncrono si es largo
  }
});
app.listen(3000, () => console.log("Webhook listo en http://localhost:3000/webhook"));
              from flask import Flask, request
import hashlib, hmac
app = Flask(__name__)
SEEN = set()
WEBHOOK_SECRET = ""  # pon tu secreto si lo configuraste en whapi.co
def verify_hmac(raw_body: bytes, signature: str) -> bool:
    if not WEBHOOK_SECRET:
        return True
    comp = hmac.new(WEBHOOK_SECRET.encode(), raw_body or b"", hashlib.sha256).hexdigest()
    return hmac.compare_digest(comp, signature or "")
@app.post("/webhook")
def webhook():
    raw = request.get_data(cache=False, as_text=False)
    if not verify_hmac(raw, request.headers.get("X-Whapi-Signature", "")):
        return ("", 401)
    ev = request.get_json(force=True, silent=True) or {}
    key = ev.get("id") or ev.get("message_id") or f'{ev.get("event_type")}:{ev.get("timestamp")}'
    if key in SEEN:
        return ("", 200)
    SEEN.add(key)
    et = ev.get("event_type")
    if et == "message_received":
        print("📥 In:", ev.get("from"), "->", ev.get("to"), "|", ev.get("type"), "|", ev.get("body"))
    elif et == "message_sent":
        print("📤 Out:", ev.get("to"), "|", ev.get("type"), "|", ev.get("status"))
    elif et == "ack_update":
        print("✅ ACK:", ev.get("id"), "->", ev.get("ack"))
    elif et == "message_reaction":
        print("😍 Reaction:", ev.get("message_id"), ev.get("reaction"), "by", ev.get("from_number"))
    elif et == "test_webhook":
        print("🧪 Test:", ev.get("message"))
    else:
        print("ℹ️ Evento desconocido:", et)
    return ("", 200)
if __name__ == "__main__":
    app.run(port=3000)
              <?php
// webhook.php
// Si usas secret en whapi.co, valida HMAC:
$WEBHOOK_SECRET = ""; // pon aquí el mismo secreto (opcional)
$raw = file_get_contents("php://input");
$ev  = json_decode($raw, true) ?: [];
function verify_hmac($raw, $secret, $headerSig) {
  if (!$secret) return true;
  $calc = hash_hmac("sha256", $raw ?: "", $secret);
  return hash_equals($calc, $headerSig ?? "");
}
if (!verify_hmac($raw, $WEBHOOK_SECRET, $_SERVER["HTTP_X_WHAPI_SIGNATURE"] ?? "")) {
  http_response_code(401); exit;
}
// Idempotencia básica (usa Redis/DB en producción)
$key = $ev["id"] ?? ($ev["message_id"] ?? (($ev["event_type"] ?? "evt").":".($ev["timestamp"] ?? time())));
$flag = sys_get_temp_dir()."/whapi_".md5($key);
if (file_exists($flag)) { http_response_code(200); exit; }
touch($flag);
// Ruteo por evento
$et = $ev["event_type"] ?? "";
switch ($et) {
  case "message_received":
    error_log("📥 In: ".$ev["from"]." -> ".$ev["to"]." | ".$ev["type"]." | ".$ev["body"]);
    break;
  case "message_sent":
    error_log("📤 Out: ".$ev["to"]." | ".$ev["type"]." | ".$ev["status"]);
    break;
  case "ack_update":
    error_log("✅ ACK: ".$ev["id"]." -> ".$ev["ack"]);
    break;
  case "message_reaction":
    error_log("😍 Reaction: ".$ev["message_id"]." ".$ev["reaction"]." by ".$ev["from_number"]);
    break;
  case "test_webhook":
    error_log("🧪 Test: ".$ev["message"]);
    break;
  default:
    error_log("ℹ️ Evento desconocido: ".$et);
}
http_response_code(200);
echo json_encode(["ok" => true]);
              5) Probar con curl
          message_received
curl -X POST https://tu-dominio.com/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "event_type":"message_received",
    "id":"SIM-1",
    "instance_id":"123",
    "chat_id":"573001112233@c.us",
    "from":"573001112233@c.us",
    "to":"573009998877@c.us",
    "from_me":0,
    "sender_name":"Cliente",
    "body":"Hola, ¿abren hoy?",
    "type":"text",
    "timestamp":1724716800,
    "is_group":0,
    "status":"received",
    "ack":"pending"
  }'
              ack_update
curl -X POST https://tu-dominio.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"event_type":"ack_update","id":"SIM-OUT-1","ack":"read"}'
              6) Buenas prácticas
- Responde 200 OK rápido; procesa en background si tu lógica tarda.
- Usa idempotencia (clave por id/message_id) para no duplicar.
- Valida HMAC si configuras secret(cabeceraX-Whapi-Signature).
- Persistencia: guarda cada evento (tu API ya inserta en messagesymessage_reactions).
- Multimedia: descarga media_urla tu storage; en tu servidor se limpia periódicamente.
- Seguridad: limita por IP, rate limit y WAF en tu reverse proxy.
