Webhooks
Un endpoint firmado por cuenta. Verificación de la firma, catálogo de eventos, política de reintentos y rotación del secreto.
Apunte WASync a su endpoint y recibirá un secreto de firma. Usa
whatsapp.events. No requiere registrar ninguna aplicación ni un paso de consentimiento.
Un solo espacio de webhook por cuenta
Una cuenta tiene exactamente una URL de webhook. PUT /webhook la establece: si
la llama de nuevo con otra URL, mueve el endpoint en lugar de añadir un segundo,
y no existe eliminación. Esto importa en cuanto más de un componente quiere
eventos: un disparador de n8n y su propio backend no pueden ser ambos
dueños del espacio. Distribuya los eventos desde su lado.
Establecer el endpoint
curl -X PUT https://developers.wasync.app/api/v1/webhook \
-H "Authorization: Bearer $WASYNC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/wasync/webhook"}'{
"url": "https://example.com/wasync/webhook",
"events": [
"message.received",
"message.status",
"connection.disconnected",
"connection.connected"
],
"secret": "whsec_3f9a…"
}Guarde el secreto. Se entrega aquí porque este es el momento en que lo necesita
para escribir su código de verificación, y GET /webhook no lo devolverá de nuevo:
ese endpoint informa { url, events, secretSet }, deliberadamente sin el
secreto, porque una ruta de lectura termina en los registros y pegada en tickets. ¿Lo perdió? Llame a
POST /webhook/rotate para obtener uno nuevo. La URL debe ser HTTPS en un host accesible
públicamente; http, localhost y los rangos privados se rechazan con
400 invalid_webhook.
La rotación no tiene ventana de solapamiento: el secreto anterior deja de valer en el instante en que se emite el nuevo. Despliegue el nuevo secreto sin demora: la política de reintentos descrita más abajo cubre un despliegue que dure segundos, no horas.
Catálogo de eventos
| Evento | Se dispara cuando | Dirección |
|---|---|---|
message.received | Llega un mensaje de WhatsApp a una conexión autorizada (texto o multimedia). | Solo entrante: los mensajes que usted envía no lo disparan. |
message.status | Cambia el estado de entrega de uno de sus mensajes salientes. | Solo saliente. |
connection.disconnected | Una conexión dejó de funcionar y su titular debe vincularla de nuevo. | Una vez por caída, no repetidamente mientras siga averiada. |
connection.connected | Una conexión que se había averiado vuelve a funcionar. | Una vez, y solo para una conexión de la que se le informó que se había averiado. |
Ignore los valores de event desconocidos en lugar de tratarlos como errores: los tipos nuevos
se anuncian en el registro de cambios antes de lanzarse.
Los eventos se disparan solo para conexiones qr. Las conexiones cloud_api no emiten
webhooks por ahora; consulte GET /messages y GET /connections/{id} para ellas.
Carga útil
{
"event": "message.received",
"connection_id": "conn_8f3a21",
"message": {
"id": "cmqj3k2ab0001xyz",
"wa_id": "[email protected]_3EB0A1B2C3",
"from": "40700000000",
"text": "Hello!",
"media_url": null,
"media_type": null,
"timestamp": 1766138640000
}
}message.id: el id estable de WASync (el mismo valor queGET /messagesdevuelve comoid). Deduplique y relacione por él: la entrega es al menos una vez.message.wa_id: el id propio de WhatsApp,nullcuando se desconoce. Solo para soporte y depuración, nunca como clave de deduplicación ni de unión.from: solo dígitos internacionales, sin+.textesnullen los mensajes que solo contienen multimedia;media_url/media_typesonnullen los mensajes de texto o cuando el archivo no se puede obtener públicamente.timestamp: epoch en milisegundos como número, no una cadena ISO.
Una entrega message.status actualiza uno de sus mensajes salientes:
{
"event": "message.status",
"connection_id": "conn_8f3a21",
"message": {
"id": "cmqj3k2ab0001xyz",
"wa_id": "[email protected]_3EB0A1B2C3",
"status": "read",
"timestamp": 1766138641000
}
}Verificar la firma
Cada entrega incluye X-WASync-Timestamp (epoch en ms) y
X-WASync-Signature: sha256=<hex>, que es
HMAC-SHA256(timestamp + "." + rawBody, secret). Calcúlela sobre el cuerpo sin procesar
(antes de cualquier análisis JSON) y compare en tiempo constante. Rechace las entregas
cuya marca de tiempo tenga más de ~5 minutos, lo que bloquea los ataques de repetición.
import crypto from "node:crypto";
// rawBody MUST be the exact bytes you received (verify BEFORE JSON.parse).
export function verifyWASync(headers, rawBody, secret) {
const ts = headers["x-wasync-timestamp"];
const sig = headers["x-wasync-signature"]; // "sha256=<hex>"
const expected =
"sha256=" +
crypto.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return Boolean(sig) &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}import express from "express";
import { verifyWASync } from "./verify.js";
const app = express();
// Capture the RAW body — signature verification needs the exact bytes.
app.post("/webhooks/wasync", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyWASync(req.headers, req.body.toString("utf8"), process.env.WASYNC_WEBHOOK_SECRET)) {
return res.status(401).end();
}
// Reject stale deliveries (replay window) — 5 minutes is a sane tolerance.
if (Math.abs(Date.now() - Number(req.headers["x-wasync-timestamp"])) > 5 * 60_000) {
return res.status(401).end();
}
res.status(200).end(); // ACK fast — under 10s — then process async
const { message } = JSON.parse(req.body.toString("utf8"));
// ... your logic (dedupe on message.id — retries can deliver duplicates)
});Política de entrega y reintentos
| Propiedad | Valor |
|---|---|
| Criterio de éxito | Cualquier 2xx. Todo lo demás, incluidas las redirecciones, es un fallo. |
| Intentos | 3 por evento: inmediato, luego tras 500 ms y luego tras 1 s. |
| Tiempo límite por intento | 10 segundos. Confirme rápido y procese de forma asíncrona. |
| Tras el último fallo | El evento se descarta. No hay una cola larga de reentrega. |
| Duplicados | Posibles. Deduplique por message.id. |
| Orden | No garantizado. Ordene por timestamp si es importante. |
| Firma en los reintentos | Se reenvían el mismo cuerpo y la misma firma: verifique cada intento de la misma manera. |
Escalera de estados
Los valores de message.status forman una escalera de un solo sentido: sent (1) → delivered (2) →
read (3), con failed como resultado terminal que no sigue a read.
Aplique las actualizaciones de forma monótona. Aplique un estado nuevo solo si su rango es mayor
que el que tiene almacenado. Un mensaje puede llegar con read antes que
delivered (una entrega tardía), y son posibles los eventos duplicados. Nunca retroceda el estado de un
mensaje en su interfaz.
Conciliación: consulte periódicamente como red de seguridad
Las entregas se intentan tres veces y luego se descartan, por lo que el manejador de su webhook es
la vía rápida, no la garantía. Ejecute un bucle en segundo plano que consulte
GET /messages?connectionId=… cada 30–60 segundos e inserte o actualice todo lo que el
manejador no haya visto, usando el cursor para obtener los datos de forma incremental. El mismo bucle
recupera las actualizaciones de estado que llegaron mientras su endpoint estaba caído.
Webhooks = actualizaciones de la interfaz en tiempo real. Consultas periódicas = corrección. Ejecute ambos.
Ciclo de vida de la conexión
{
"event": "connection.disconnected",
"connection_id": "conn_8f2a…",
"connection": {
"id": "conn_8f2a…",
"phone_number": "393331234567",
"label": "Studio Rossi",
"status": "disconnected",
"needs_reconnect": true,
"license_status": "active",
"license_expires": "2027-08-08T09:00:00.000Z",
"reason": "AUTH_LOST"
},
"timestamp": 1786000000000
}Una conexión qr puede dejar de funcionar por motivos ajenos al control de cualquiera: el
teléfono se apaga durante un día, se abre WhatsApp en otro dispositivo, la
sesión caduca. El número queda entonces en silencio en ambas direcciones, y sin un
evento, la primera persona en notarlo es su cliente.
El código QR no está en la carga útil, a propósito. Un QR de WhatsApp es una credencial de
inicio de sesión por escaneo: cualquiera que lo lea puede tomar el control de la sesión. El
evento le indica que hace falta una nueva vinculación: obtenga el QR mediante su
GET /connections/{id} autenticado y muéstreselo al titular del número, exactamente
como en la configuración inicial. Guíe su interfaz con needs_reconnect; trate reason como una
pista de registro orientativa, ya que pueden aparecer valores nuevos.
Latencia de detección: una desconexión se detecta mediante una sonda de estado de 5 minutos (~5 min). Cuando la sesión parece recuperable, primero se intenta una reparación automática, de modo que ese camino tarda ~15–20 minutos.
¿No recibe eventos?
- ¿La URL de entrega es HTTPS en un host público? Localhost, las IP privadas y
http://se rechazan. Use un túnel durante el desarrollo y compruebe qué es lo que se almacenó realmente conGET /webhook. - ¿La credencial incluye
whatsapp.eventsy cubre la conexión en la que llegó el mensaje? - ¿La licencia de la conexión sigue activa? Una licencia vencida significa que no hay eventos
y
402en los envíos. - ¿Su endpoint responde
2xxen menos de 10 segundos? Los manejadores lentos parecen fallos y consumen los tres intentos. - ¿Es una conexión
qr? Las conexionescloud_apino emiten webhooks. - Solo los mensajes entrantes disparan
message.received: sus propios envíos nunca lo hacen.