¿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
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.