Por qué verificar
La URL de tu webhook es pública: cualquiera que la conozca puede enviarte un POST. Sin firma, un atacante podría inyectar leads o eventos falsos en tu sistema.
Lectico firma cada payload con HMAC-SHA256 usando el secret que recibiste al crear la suscripción. Antes de confiar en un webhook, debes verificar esa firma.
Cabecera
Cada POST llega con:
Lectico-Signature: t=1744550460,v1=a3f1...c2d8t— timestamp Unix del momento del envío.v1— firma hex:HMAC_SHA256(secret, "<t>.<body_raw>").
Verificación en Node
Lo más cómodo es usar el SDK:
import express from "express";
import { constructEvent, WebhookSignatureError } from "@lectico/api/webhooks";
const app = express();
app.post(
"/webhooks/lectico",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = constructEvent(
req.body,
req.header("Lectico-Signature"),
process.env.LECTICO_WEBHOOK_SECRET!,
);
switch (event.type) {
case "lead.captured":
// envia a tu CRM
break;
case "agent.created":
// notifica al equipo
break;
}
res.status(200).json({ received: true });
} catch (err) {
if (err instanceof WebhookSignatureError) {
return res.status(400).send(`Invalid signature: ${err.message}`);
}
throw err;
}
},
);constructEvent valida:
- Que la cabecera
t=...,v1=...esté bien formada. - Que el HMAC coincida con el body exacto (por eso se usa
express.raw, noexpress.json). - Que el timestamp esté dentro de una tolerancia de 5 minutos (configurable con
toleranceSeconds). Esto bloquea ataques de replay.
Verificación manual (otros lenguajes)
Si no usas el SDK, el algoritmo es el mismo en cualquier lenguaje:
import hmac, hashlib, time
def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t, v1 = int(parts["t"]), parts["v1"]
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(
secret.encode(),
f"{t}.{body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, v1)No uses == para comparar firmas. Es vulnerable a timing attacks. Usa hmac.compare_digest (Python), crypto.timingSafeEqual (Node) o equivalente.
Errores comunes
bodyparseado como JSON antes de verificar — el hash se calcula sobre el body crudo, no sobre el objeto. Si tu framework ya lo parseó, la firma nunca coincidirá.- Reconstruir el JSON — tampoco funciona: cualquier reordenación de claves o espacios cambia el hash.
- Secret incorrecto — asegúrate de que la variable de entorno coincida con el
secretque te dioPOST /v1/webhooks. - Timestamp fuera de tolerancia — ocurre si tu servidor tiene el reloj desincronizado (usa NTP).