¿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 | pending al 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
messages
ymessage_reactions
). - Multimedia: descarga
media_url
a tu storage; en tu servidor se limpia periódicamente. - Seguridad: limita por IP, rate limit y WAF en tu reverse proxy.