Webhooks de Webflow: recepción y verificación
Implementación completa para AgriTech Koala — Node.js + Express · HMAC-SHA256 · ventana anti-replay
Webflow
POST
POST
Verificar
HMAC
HMAC
Anti-replay
5 min
5 min
Router
eventos
eventos
Acción
DB / CDN
DB / CDN
200 OK
ACK
ACK
Firma HMAC-SHA256
Se calcula
HMAC("ts:body") con el secreto de Webflow y se compara con x-webflow-signature.
Ventana anti-replay
Se rechaza cualquier petición cuyo
x-webflow-timestamp difiera más de 5 minutos del reloj del servidor.
Tiempo constante
crypto.timingSafeEqual() previene ataques de temporización en la comparación de firmas.
const crypto = require('crypto'); /** * Verifica la firma HMAC-SHA256 del webhook de Webflow. * @param rawBody Cuerpo crudo (Buffer / string) * @param signature Cabecera x-webflow-signature * @param timestamp Cabecera x-webflow-timestamp (epoch ms) * @param secret WEBFLOW_WEBHOOK_SECRET */ function verifyWebflowSignature(rawBody, signature, timestamp, secret) { // 1. Ventana anti-replay de 5 minutos const now = Date.now(); if (Math.abs(now - parseInt(timestamp, 10)) > 300_000) { return false; } // 2. HMAC-SHA256("timestamp:rawBody") const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}:${rawBody}`) .digest('hex'); // 3. Comparación en tiempo constante (previene timing attacks) try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } catch { return false; // Longitudes distintas → inválido } }
router.post( '/webhooks/webflow', express.raw({ type: 'application/json' }), // ← raw body obligatorio async (req, res) => { const signature = req.headers['x-webflow-signature']; const timestamp = req.headers['x-webflow-timestamp']; if (!signature || !timestamp) return res.status(400).json({ error: 'Cabeceras ausentes' }); const valid = verifyWebflowSignature( req.body.toString(), signature, timestamp, process.env.WEBFLOW_WEBHOOK_SECRET ); if (!valid) return res.status(401).json({ error: 'Firma inválida' }); const event = JSON.parse(req.body); switch (event.triggerType) { case 'form_submission': await handleFormSubmission(event.payload, req.db); // → INSERT lead break; case 'ecomm_new_order': await handleNewOrder(event.payload, req.db); // → Activar licencia break; case 'site_publish': await handleSitePublish(event.payload); // → Purgar caché CDN break; } res.status(200).json({ ok: true }); // Siempre 200 → evita reintentos } );
Eventos configurados
| triggerType | Descripción | Acción en AgriTech Koala | Estado |
|---|---|---|---|
| form_submission | Lead rellena formulario de demo | INSERT en leads · notificar equipo ventas |
Activo |
| ecomm_new_order | Compra de licencia / curso | INSERT en licenses · email credenciales |
Activo |
| site_publish | Publicación del sitio Webflow | Purge Cloudflare CDN purge_everything |
Activo |
| collection_item_created | Nuevo artículo CMS | Sync Algolia · reindex | Pendiente |
| collection_item_changed | Artículo CMS actualizado | Update Algolia index | Pendiente |
| ecomm_order_changed | Estado pedido cambia | Actualizar estado licencia | Pendiente |
# 1. Generar firma (Node REPL) const ts = Date.now().toString(); const body = JSON.stringify({ triggerType: 'form_submission', payload: { data: { name: 'Ana García', email: 'ana@demo.com', company: 'Koala Demo' } } }); const sig = crypto.createHmac('sha256', 'whsec_k0AlA_s3cR3T_4gR1T3cH') .update(`${ts}:${body}`).digest('hex'); # 2. Enviar petición curl -X POST http://localhost:3000/webhooks/webflow \ -H "Content-Type: application/json" \ -H "x-webflow-signature: <sig>" \ -H "x-webflow-timestamp: <ts>" \ -d '<body>' # Respuesta esperada: {"ok":true}