Desarrollo a medida
Webhooks de HubSpot: firmas, idempotencia y reintentos
Validar la firma v3, responder rápido, procesar cada evento una sola vez y sobrevivir a los reintentos: lo que necesita un receptor de webhooks de HubSpot en producción.
Para que los webhooks de HubSpot sean fiables necesitas cuatro cosas: validar la firma de cada petición, responder en menos de 5 segundos, procesar cada evento una sola vez aunque llegue repetido y tener un plan para los fallos. Si falta cualquiera de ellas, tarde o temprano verás facturas duplicadas, contactos sin sincronizar o un endpoint saturado por reintentos.
Esta guía resume cómo lo montamos en proyectos reales: qué envía HubSpot, cómo comprobar que la petición es suya, cómo diseñar la clave de idempotencia y qué hacer cuando el sistema de destino no responde.
¿Qué envía HubSpot en un webhook?
Cuando una app (privada, de proyecto o pública) se suscribe a eventos del CRM, HubSpot hace un POST a tu URL con un array JSON. Cada petición puede agrupar hasta 100 eventos si se producen muchos en poco tiempo, así que tu código nunca debe asumir que recibe uno solo.
Cada evento incluye, entre otros, subscriptionType (por ejemplo, cambio de propiedad en negocios), objectId, propertyName y propertyValue si es un cambio de propiedad, occurredAt en milisegundos, attemptNumber (empieza en 0) y changeSource, que indica si el cambio vino de la interfaz, de una importación, de la API o de una integración.
El webhook avisa de que algo cambió, pero no es la fuente de verdad. Para datos que importan (importes, líneas de pedido, estado de una factura) vuelve a leer el registro desde la API antes de actuar.
Cómo validar la firma v3 paso a paso
Cualquiera que conozca tu URL puede enviarte un JSON con forma de webhook. La firma es lo que demuestra que la petición viene de HubSpot. En las peticiones firmadas con la versión 3 llegan dos cabeceras: X-HubSpot-Signature-v3 y X-HubSpot-Request-Timestamp.
- Rechaza la petición si la marca de tiempo tiene más de 5 minutos. Así frenas ataques de repetición.
- Construye la cadena concatenando, en este orden, el método HTTP, la URI completa (con la query), el cuerpo tal cual llegó y la marca de tiempo.
- Calcula un HMAC SHA-256 de esa cadena en UTF-8 usando como clave el secreto de cliente de la app y codifícalo en Base64.
- Compara el resultado con la cabecera usando una comparación de tiempo constante, nunca un
==de cadenas.
En Node.js, el núcleo cabe en una línea: crypto.createHmac('sha256', secret).update(method + uri + rawBody + timestamp).digest('base64'). El detalle que más fallos provoca es el cuerpo: tienes que firmar el texto crudo recibido, no el JSON ya parseado y vuelto a serializar, porque cambiar un espacio cambia la firma. El segundo es la URI: si tu servidor está detrás de un proxy, reconstruye la URL pública que llamó HubSpot, no la interna. La guía oficial de validación de peticiones detalla además qué caracteres codificados hay que decodificar en la URI y cómo se validan las firmas v1 y v2, que todavía aparecen en algunos tipos de petición.
Responde en menos de 5 segundos y procesa después
HubSpot considera fallida una entrega si tardas más de 5 segundos en contestar a un lote. Si en ese tiempo intentas crear una factura en el ERP, subir un PDF y actualizar tres propiedades, cualquier lentitud del otro sistema se convierte en un reintento, y el reintento en un posible duplicado.
El patrón que usamos es siempre el mismo: validar la firma, guardar los eventos en una cola o tabla persistente, devolver un 200 y procesar en segundo plano. El receptor no hace nada más.
- 01Evento en HubSpot
- 02Firma v3 validada
- 03Evento guardado en cola
- 04Respuesta 200 inmediata
- 05Proceso idempotente en segundo plano
- 06Registro y alerta si falla
Idempotencia: el mismo evento llegará dos veces
Con reintentos de por medio, recibir un evento repetido no es una rareza, es parte del contrato. Un proceso idempotente es el que produce el mismo resultado lo ejecutes una vez o cinco. Para conseguirlo necesitas una clave que identifique el evento y un registro de las claves ya procesadas.
Cómo construir la clave
La tentación es usar eventId, pero la propia documentación de HubSpot advierte de que no está garantizado como único. Es más robusto componer la clave con subscriptionType, objectId, propertyName, propertyValue y occurredAt. Guárdala en una tabla con restricción de unicidad: si la inserción falla porque ya existe, el evento se descarta sin más.
Idempotencia también hacia el otro sistema
La clave del evento evita procesar dos veces la misma notificación, pero no protege de dos eventos distintos que piden lo mismo (por ejemplo, un negocio que pasa a ganado, vuelve atrás y se gana otra vez). Para eso, guarda en HubSpot el identificador del documento creado fuera, como el número de factura o el ID del pedido, y comprueba si ya existe antes de crear. Si la API de destino admite cabecera de idempotencia, úsala con una clave derivada del negocio.
En las integraciones de facturación HubSpot ↔ Holded que hemos montado para varias empresas, esta doble barrera es la que impide que una factura se emita dos veces cuando alguien mueve un negocio de etapa más de una vez.
Reintentos: los de HubSpot y los tuyos
Hay dos tipos de reintento y conviene no mezclarlos.
- Los de HubSpot hacia ti. Según la documentación, una notificación fallida se reintenta hasta 10 veces, con esperas variables y cierta aleatoriedad. Cuenta como fallo un error de conexión, un timeout o cualquier respuesta 4xx o 5xx. Por eso no devuelvas un 400 para eventos que simplemente no te interesan: responde 200 y descártalos.
- Los tuyos hacia los demás. Cuando tu proceso llama a la API de HubSpot o a la del ERP y recibe un 429 o un 5xx, reintenta con espera exponencial y algo de aleatoriedad (1, 2, 4, 8 segundos…) y respeta la cabecera
Retry-Aftersi viene. Pon un máximo de intentos y, al agotarlo, manda el evento a una cola de errores.
Los límites de la API cuentan aquí. Una app de distribución privada tiene un límite por ráfaga cada 10 segundos y un límite diario compartido por la cuenta, que dependen de la edición. Si cada webhook dispara cinco llamadas, una importación masiva puede agotar la ráfaga en segundos. Agrupa lecturas con los endpoints batch y controla la concurrencia de tus workers. Las cifras actualizadas están en las directrices de uso de la API.
¿Qué pasa con el orden de los eventos?
HubSpot no garantiza que los eventos lleguen en el orden en que ocurrieron. Si un comercial cambia el importe de un negocio dos veces en un minuto, puedes recibir primero el segundo cambio. Hay dos defensas sencillas:
- Guarda el
occurredAtdel último cambio aplicado por registro y propiedad, e ignora eventos más antiguos. - Usa el evento solo como aviso y lee el estado actual del registro antes de escribir en el otro sistema. Es una llamada más, pero elimina toda una categoría de errores.
Lo que separa un webhook de prueba de uno en producción
Antes
- Proceso dentro de la petición
- Sin validar la firma
- eventId como clave única
- Errores que se pierden en un log
Después
- Cola y respuesta inmediata
- Firma v3 y marca de tiempo comprobadas
- Clave compuesta con restricción de unicidad
- Cola de errores con alerta y reproceso
Además de lo anterior, en cada proyecto añadimos tres piezas que se agradecen el día que algo falla:
- Registro de cada evento con su clave, estado y error, consultable sin entrar al servidor.
- Reproceso manual de un evento o de un rango de fechas, que solo es seguro si todo es idempotente.
- Modo simulación (dry-run) que procesa eventos reales sin escribir en el destino, para probar cambios con datos de verdad.
Si además la sincronización va en los dos sentidos, aparece un problema nuevo: los bucles, cuando tu propia escritura dispara otro webhook. Lo tratamos en detalle en cómo montar una sincronización bidireccional sin duplicados ni bucles. Y si todavía no tienes claro desde qué tipo de app suscribirte, empieza por las diferencias entre apps privadas, de proyecto y públicas.
Checklist rápida
| Punto | Qué comprobar |
|---|---|
| Firma | v3 validada sobre el cuerpo crudo y la URI pública, con comparación de tiempo constante |
| Marca de tiempo | Rechazo de peticiones con más de 5 minutos |
| Respuesta | 200 en menos de 5 segundos, sin lógica de negocio dentro |
| Lotes | El código itera un array de hasta 100 eventos |
| Idempotencia | Clave compuesta con unicidad y comprobación previa en el destino |
| Reintentos propios | Backoff exponencial, Retry-After y cola de errores |
| Orden | occurredAt guardado o relectura del registro |
Este es el estándar con el que trabajamos en Vexco, tanto en el desarrollo a medida sobre HubSpot como en las integraciones con otros sistemas, y es lo primero que revisamos cuando nos llega una integración heredada que falla de forma intermitente. Si quieres ver un proyecto grande, el caso de la SaaS que migró desde Salesforce reúne una docena de microservicios conectados a HubSpot.