HubSpot Webhooks
Patrones de codigo listos para recibir y verificar webhooks de HubSpot CRM (firma HMAC-SHA256 v3) en Express y FastAPI, con manejo de eventos de contactos, empresas y deals.
Descarga abierta · sin registro · para Node.js, Python, Express
// Generated with: hubspot-webhooks skill // https://github.com/hookdeck/webhook-skills // // ───────────────────────────────────────────────────────────────────────────── // GrowSaaS — HubSpot Webhook Handler (Express + Node.js 20) // Sincroniza eventos CRM de HubSpot con la base de datos de GrowSaaS // CULTIVA IA · Automatizaciones · 2026 // ─────────────────────────────────────────────────────────────────────────────
'use strict';
const crypto = require('crypto'); const express = require('express');
// ────────────────────────────────────────────────────────── // CONFIGURACIÓN // ────────────────────────────────────────────────────────── const PORT = process.env.PORT || 3000; const HUBSPOT_SECRET = process.env.HUBSPOT_CLIENT_SECRET; // App Client Secret const MAX_TIMESTAMP_AGE = 5 * 60 * 1000; // 5 minutos (ms)
// Simulación de dependencias externas (en producción: imports reales)
const db = { users: { create: async (d) => ({ id: usr_${d.contactId}, ...d }) },
workspaces: { create: async (d) => ({ id: ws_${d.companyId}, ...d }) },
tickets: { create: async (d) => ({ id: tkt_${d.objectId}, ...d }) } };
const redis = { get: async (_k) => null, set: async (_k, _v, _ttl) => {} };
const email = { sendWelcome: async (d) => console.log([email] bienvenida → ${d.contactId}) };
const crm = { updatePipeline: async (d) => console.log([crm] pipeline → ${JSON.stringify(d)}) };
const onboarding = { activate: async (d) => console.log([onboarding] activar → deal ${d.dealId}) };
const support = { openTicket: async (d) => console.log([support] ticket → ${d.objectId}) };
// ────────────────────────────────────────────────────────── // LOGGING ESTRUCTURADO (JSON para Datadog / cualquier SIEM) // ────────────────────────────────────────────────────────── const log = { info: (msg, meta = {}) => console.log(JSON.stringify({ level: 'info', msg, ...meta, ts: new Date().toISOString() })), warn: (msg, meta = {}) => console.log(JSON.stringify({ level: 'warn', msg, ...meta, ts: new Date().toISOString() })), error: (msg, meta = {}) => console.log(JSON.stringify({ level: 'error', msg, ...meta, ts: new Date().toISOString() })), };
// ────────────────────────────────────────────────────────── // VERIFICACIÓN DE FIRMA HMAC-SHA256 v3 // ────────────────────────────────────────────────────────── // HubSpot firma: HMAC-SHA256(base64) de → METHOD + URI + rawBody + timestamp // Cabeceras: X-HubSpot-Signature-v3 / X-HubSpot-Request-Timestamp
/**
- Verifica la firma de un webhook de HubSpot (versión 3).
- @param {object} params
- @param {string} params.method - HTTP method (ej. "POST")
- @param {string} params.uri - URL completa que firmó HubSpot
- @param {Buffer} params.rawBody - Cuerpo sin parsear (Buffer)
- @param {string} params.timestamp - Timestamp en milisegundos (string)
- @param {string} params.signature - Valor de X-HubSpot-Signature-v3
- @param {string} params.secret - Client Secret de la app HubSpot
- @returns {boolean} */ function verifyHubSpotWebhook({ method, uri, rawBody, timestamp, signature, secret }) { if (!signature || !timestamp || !secret) return false;
// Rechazar requests con timestamp demasiado antiguo (replay attack) const ts = Number(timestamp); if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > MAX_TIMESTAMP_AGE) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;
const signedStr = ${method}${uri}${body}${timestamp};
const expected = crypto.createHmac('sha256', secret)
.update(signedStr, 'utf8')
.digest('base64');
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
} catch {
return false;
}
}
// ────────────────────────────────────────────────────────── // MIDDLEWARE: verificar firma + idempotencia // ──────────────────────────────────────────────────────────
async function hubspotSignatureMiddleware(req, res, next) {
const signature = req.headers['x-hubspot-signature-v3'];
const timestamp = req.headers['x-hubspot-request-timestamp'];
const uri = ${req.protocol}://${req.get('host')}${req.originalUrl};
const valid = verifyHubSpotWebhook({ method: req.method, uri, rawBody: req.body, // Buffer (gracias a express.raw) timestamp, signature, secret: HUBSPOT_SECRET, });
if (!valid) { log.warn('Firma HubSpot inválida', { uri, timestamp }); return res.status(400).json({ error: 'Firma inválida' }); }
// Parsear eventos DESPUÉS de verificar la firma try { req.hubspotEvents = JSON.parse(req.body.toString()); } catch { return res.status(400).json({ error: 'Body no es JSON válido' }); }
next(); }
// ────────────────────────────────────────────────────────── // HANDLERS DE EVENTOS (uno por subscriptionType) // ──────────────────────────────────────────────────────────
// contact.creation → crear usuario + email de bienvenida async function handleContactCreation(event) { const { objectId: contactId, portalId, eventId } = event;
const user = await db.users.create({ contactId, portalId, createdAt: new Date() }); await email.sendWelcome({ contactId });
log.info('Contacto creado', { contactId, userId: user.id, eventId }); }
// contact.propertyChange → actualizar lifecycle stage en pipeline async function handleContactPropertyChange(event) { const { objectId: contactId, propertyName, propertyValue, eventId } = event;
if (propertyName !== 'lifecyclestage') return; // Solo nos interesa este campo
await crm.updatePipeline({ contactId, stage: propertyValue }); log.info('Lifecycle stage actualizado', { contactId, stage: propertyValue, eventId }); }
// deal.propertyChange → activar onboarding si deal cerrado ganado async function handleDealPropertyChange(event) { const { objectId: dealId, propertyName, propertyValue, eventId } = event;
if (propertyName !== 'dealstage' || propertyValue !== 'closedwon') return;
await onboarding.activate({ dealId }); log.info('Onboarding activado', { dealId, eventId }); }
// company.creation → crear workspace en GrowSaaS async function handleCompanyCreation(event) { const { objectId: companyId, portalId, eventId } = event;
const ws = await db.workspaces.create({ companyId, portalId, createdAt: new Date() }); log.info('Workspace creado', { companyId, workspaceId: ws.id, eventId }); }
// ticket.creation → abrir incidencia en soporte interno async function handleTicketCreation(event) { const { objectId, eventId } = event;
await support.openTicket({ objectId }); log.info('Ticket de soporte creado', { objectId, eventId }); }
// ────────────────────────────────────────────────────────── // ROUTER DE EVENTOS (dispatch por subscriptionType) // ──────────────────────────────────────────────────────────
const EVENT_HANDLERS = { 'contact.creation': handleContactCreation, 'contact.propertyChange': handleContactPropertyChange, 'deal.propertyChange': handleDealPropertyChange, 'company.creation': handleCompanyCreation, 'ticket.creation': handleTicketCreation, };
/**
Procesa un array de eventos HubSpot con idempotencia por eventId.
Usa setImmediate para no bloquear la respuesta HTTP. */ async function processEvents(events) { for (const event of events) { const { eventId, subscriptionType } = event;
// Idempotencia: si ya procesamos este eventId, saltar const key =
hs:evt:${eventId}; const exists = await redis.get(key); if (exists) { log.info('Evento duplicado ignorado', { eventId, subscriptionType }); continue; }const handler = EVENT_HANDLERS[subscriptionType]; if (!handler) { log.warn('subscriptionType no manejado', { subscriptionType, eventId }); continue; }
try { await handler(event); // Marcar como procesado (TTL 24 h para cubrir reintentos de HubSpot) await redis.set(key, '1', 86400); } catch (err) { log.error('Error procesando evento', { subscriptionType, eventId, error: err.message, stack: err.stack, }); // No lanzar: seguimos con el siguiente evento del batch } } }
// ────────────────────────────────────────────────────────── // ROUTE HANDLER // ──────────────────────────────────────────────────────────
async function hubspotWebhookRoute(req, res) { // Responder 200 inmediatamente (HubSpot no espera procesamiento) res.status(200).send('OK');
// Procesar en background sin bloquear el event loop setImmediate(() => { processEvents(req.hubspotEvents).catch((err) => { log.error('Error inesperado en processEvents', { error: err.message }); }); }); }
// ────────────────────────────────────────────────────────── // BOOTSTRAP DE EXPRESS // ──────────────────────────────────────────────────────────
const app = express();
// CRÍTICO: express.raw() → HubSpot necesita el body sin parsear para HMAC app.post( '/webhooks/hubspot', express.raw({ type: 'application/json' }), hubspotSignatureMiddleware, hubspotWebhookRoute, );
// Health check para Railway app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'growsaas-webhook' }));
app.listen(PORT, () => { log.info('Servidor de webhooks arrancado', { port: PORT, env: process.env.NODE_ENV }); });
module.exports = { app, verifyHubSpotWebhook }; // Para tests
// ────────────────────────────────────────────────────────── // DESARROLLO LOCAL // ────────────────────────────────────────────────────────── // // 1. Copia .env.example → .env y rellena HUBSPOT_CLIENT_SECRET // 2. Lanza el túnel Hookdeck: // npx hookdeck-cli listen 3000 hubspot --path /webhooks/hubspot // 3. Pega la URL de Hookdeck en tu app HubSpot → Webhooks → Target URL // 4. node webhook.js // // ────────────────────────────────────────────────────────── // VARIABLES DE ENTORNO REQUERIDAS // ────────────────────────────────────────────────────────── // // HUBSPOT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxx # App Client Secret (no el token de app privada) // PORT=3000 // NODE_ENV=production
// qué_hace
Proporciona implementaciones de verificacion de firma y handlers de eventos para webhooks de HubSpot CRM.
// cómo_lo_hace
Usa HMAC-SHA256 con timestamp para verificar la cabecera X-HubSpot-Signature-v3 y enruta eventos por subscriptionType.
// ejemplo_de_uso
Para backends que reaccionan a eventos de HubSpot como contactos creados o deals cerrados en tiempo real. Ej.: verificar la firma X-HubSpot-Signature-v3 en tu handler de FastAPI y enrutar el evento deal.created a tu sistema de onboarding automático.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.