Webhooks da HubSpot: assinatura v3, retries e dead-letter
Rodar webhooks da HubSpot em produção se resume a três disciplinas: validar toda requisição com a assinatura v3 (HMAC SHA-256 sobre método + URI + corpo + timestamp), tratar a entrega como at-least-once e fora de ordem (então o seu handler precisa ser idempotente) e persistir os eventos antes de processar, porque a HubSpot só refaz entregas que falharam 10 vezes ao longo de 24 horas e não oferece fila de dead-letter: depois dessa janela, o evento se foi. Este guia cobre as três, com as regras exatas, o código e os modos de falha que já depuramos em portais reais.
Os fatos que moldam tudo (verificados contra a documentação da HubSpot e a comunidade):
- Assinatura v3: HMAC SHA-256, codificada em Base64, cabeçalhos
X-HubSpot-Signature-v3+X-HubSpot-Request-Timestamp; rejeite timestamps com mais de 5 minutos. - Janela de resposta: seu endpoint precisa responder em ~5 segundos; os lotes carregam até 100 eventos; a concorrência é de 10 requisições simultâneas por padrão e é configurável (
throttling.maxConcurrentRequests, mínimo acima de 5). - Entrega: at-least-once (duplicados acontecem) e a ordem não é garantida; sequencie pelo
occurredAt. - Retries: até 10 tentativas ao longo de 24 horas, disparadas por falhas de conexão, timeouts e qualquer resposta 4xx/5xx; não é configurável e não há replay manual. (As ações de webhook em workflows seguem regras diferentes.)
Validação de assinatura: v1, v2, v3, e por que os bytes crus importam
A HubSpot assina as requisições de webhook para que seu endpoint possa provar que elas vieram mesmo da HubSpot e não foram adulteradas. Existem três versões em circulação, e saber qual se aplica onde economiza horas de dor de cabeça:
- v1 (
X-HubSpot-Signature, cabeçalho de versão = v1): hash SHA-256 declient_secret + corpo_da_requisicao, em hexadecimal. Usada pelas assinaturas clássicas de webhook de objetos do CRM. - v2: hash SHA-256 de
client_secret + metodo_http + URI + corpo_da_requisicao. Usada pelas ações de webhook em workflows e pelos CRM cards. - v3 (
X-HubSpot-Signature-v3): HMAC SHA-256 com o client secret como chave, sobremetodo + URI + corpo + timestamp, em Base64, com o timestamp chegando emX-HubSpot-Request-Timestamp. Esta é a recomendação atual, e a única versão com proteção contra replay: rejeite qualquer requisição cujo timestamp tenha mais de 5 minutos.
Uma validação v3 funcional em Node, usando o corpo cru da requisição:
const crypto = require('crypto');
function isValidHubSpotRequest(req, rawBody, clientSecret) {
const timestamp = req.headers['x-hubspot-request-timestamp'];
const received = req.headers['x-hubspot-signature-v3'];
if (!timestamp || !received) return false;
// HubSpot timestamps are in MILLISECONDS
const ts = parseInt(timestamp, 10);
if (!Number.isFinite(ts) || Date.now() - ts > 5 * 60 * 1000) return false;
const uri = `https://${req.headers.host}${req.url}`;
const base = `${req.method}${uri}${rawBody}${timestamp}`;
const expected = crypto
.createHmac('sha256', clientSecret)
.update(base, 'utf8')
.digest('base64');
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
- Valide os bytes crus, não o corpo parseado. Se algum middleware parseia e re-serializa o JSON, faz trim de strings ou normaliza espaços antes da validação, o hash muda e a validação falha de forma intermitente, só nos payloads que contêm o que quer que o middleware “conserte”. Um caso real da comunidade da HubSpot: um hook global fazia trim das strings de entrada, um valor de webhook tinha um espaço no final, e as assinaturas divergiam “aleatoriamente” durante meses. Capture o corpo cru antes de qualquer processamento.
- Reconstrua a URI exatamente como a HubSpot chamou. Atrás de proxies e load balancers, o scheme, o host ou a porta podem diferir do que o seu framework reporta. Se a validação falha de forma consistente, logue a URI que você está montando e compare com a URL de destino configurada no app.
- O timestamp é em milissegundos. Compará-lo contra relógios em segundos faz toda requisição parecer antiga, e tudo é rejeitado.
- Use comparação em tempo constante na verificação final, como no trecho acima, para não vazar informação da assinatura byte a byte.
O client secret usado como chave do HMAC é o secret do seu app, o mesmo das configurações de autenticação; nosso guia de autenticação e apps na API da HubSpot mostra onde ele mora e como guardá-lo com segurança.
O modelo de entrega: at-least-once, fora de ordem e em lotes
Desenhar o handler começa por aceitar três propriedades do modelo de entrega da HubSpot que você não pode mudar:
- Entrega at-least-once. Duplicados não são bug; são parte do contrato. Uma entrega que dá timeout depois de o seu servidor já ter processado será retentada, e você verá o mesmo evento de novo.
- Nenhuma garantia de ordem. Um evento de
propertyChangepode chegar antes do evento decreationdo mesmo registro. A lógica de sequência precisa usar o timestampoccurredAtdentro de cada evento, nunca a ordem de chegada. - Lotes de até 100 eventos. Uma única requisição HTTP pode carregar uma mistura de tipos de assinatura e registros. Processe os eventos individualmente dentro do lote, para que um evento ruim não derrube os outros 99 (mais sobre isso na seção de dead-letter).
E a restrição operacional que molda a arquitetura: responda rápido, em cerca de 5 segundos. O padrão que sobrevive à produção é ack primeiro, processamento depois: valide a assinatura, persista os eventos crus, retorne 200 imediatamente e faça o trabalho de verdade (chamadas de API, enriquecimento, escritas de volta no CRM via API) de forma assíncrona, a partir de uma fila. Um handler que faz processamento pesado inline vai estourar o tempo sob carga, disparar retries e amplificar o próprio tráfego exatamente quando as coisas estão piores.
Idempotência: a disciplina que torna os duplicados inofensivos
Como os duplicados são garantidos em algum momento, o handler precisa produzir o mesmo estado final não importa quantas vezes um evento chegue. A receita prática:
- Monte uma chave de deduplicação com
eventId+portalId(adicionandoattemptNumberse quiser rastrear reentregas separadamente) e registre as chaves processadas em um armazenamento com TTL maior que a janela de retry de 24 horas. Viu a chave duas vezes? Dê o ack e pule. - Torne as escritas condicionais. Busque antes de criar registros, use semântica de upsert onde existir e cheque o estado antes de efeitos colaterais (“esse negócio já está marcado como sincronizado?”). O mesmo princípio que aplicamos às custom code actions que sofrem retry: tudo que pode rodar duas vezes precisa ser seguro para rodar duas vezes.
- Nunca dispare efeitos colaterais não idempotentes direto do evento. E-mails, faturas e notificações passam pela fila deduplicada, não saem direto do handler HTTP.
- Resolva corridas com o
occurredAt. Quando duas mudanças de propriedade do mesmo registro chegam fora de ordem, vence a última escrita pelo timestamp do evento, não pelo horário de chegada.
Dead-letter: a fila que a HubSpot não te dá
Aqui está o fato que deveria guiar a sua arquitetura: a HubSpot refaz uma entrega que falhou até 10 vezes ao longo de 24 horas, e depois o evento se foi. Não existe fila de dead-letter do lado da HubSpot, nem painel de entregas falhas para reprocessar, nem botão de retry manual. Se o seu endpoint caiu num deploy que deu errado na sexta à noite e ficou fora do ar até domingo, os dados de segunda simplesmente não existem, a menos que você tenha construído para isso.
Construir para isso significa duas camadas:
- Persistir primeiro, processar depois. As únicas tarefas do handler HTTP são validar, guardar o evento cru e dar o ack. Com todo evento armazenado de forma durável antes do processamento, um bug de processamento nunca perde dados: você corrige o bug e reprocessa a partir do seu próprio armazenamento. Isso transforma a janela de 24 horas da HubSpot em “para sempre” para todo evento que chegou até você.
- Sua própria fila de dead-letter para falhas de processamento. Eventos que falham no processamento depois de N retries internos vão para uma tabela ou fila de DLQ com o erro anexado, alerta incluído. E, ponto crucial, processe os eventos individualmente: com lotes de até 100, um handler ingênuo que retorna 500 porque um evento veio malformado força a reentrega dos 100, multiplicando duplicados e carga.
E para a lacuna que a persistência não cobre (eventos emitidos enquanto seu endpoint esteve completamente inacessível além da janela de retry), a recuperação é a reconciliação por backfill: um job agendado que consulta a Search API por registros modificados desde a última sincronização sabidamente boa e cura o delta. Os webhooks te mantêm em tempo real; o backfill te mantém honesto. A arquitetura de eventos mais ampla está no nosso guia de automação na HubSpot com webhooks, workflows e código personalizado.
Uma dica de quem já se queimou: os piores incidentes de webhook que já resgatamos tinham o mesmo formato: ninguém percebeu que as entregas estavam falhando até a janela de retry expirar. A HubSpot não vai te acordar quando o seu endpoint responder 500. Monitore do seu lado: alerta na taxa de erro do endpoint, na profundidade da fila e, o mais subestimado, no silêncio. Se uma assinatura que normalmente entrega centenas de eventos por hora entrega zero por trinta minutos, algo quebrou lá em cima, e cada minuto quieto é dado que você pode estar perdendo.
Checklist de produção
- Assinatura v3 validada sobre o corpo cru, comparação em tempo constante, timestamp rejeitado depois de 5 minutos (em milissegundos).
- Ack em até 5 segundos: validar, persistir o evento cru, retornar 200; todo o processamento assíncrono.
- Chave de deduplicação (eventId + portalId) com TTL além de 24 horas.
- Eventos processados individualmente dentro dos lotes; um evento ruim nunca derruba os outros 99.
- Ordenação pelo
occurredAt, nunca pela chegada. - DLQ interna com contexto de erro e alertas para falhas de processamento.
- Backfill de reconciliação agendado para quedas além da janela de retry.
- Monitoramento de taxa de erro, profundidade de fila e silêncio de eventos.
Perguntas frequentes
Como validar a assinatura de um webhook da HubSpot?
Use a v3: calcule um HMAC SHA-256 (chave = o client secret do seu app) sobre a concatenação de método HTTP, URI completa, corpo cru da requisição e o valor do cabeçalho X-HubSpot-Request-Timestamp, codifique em Base64 e compare com o cabeçalho X-HubSpot-Signature-v3 usando comparação em tempo constante. Rejeite requisições cujo timestamp tenha mais de 5 minutos e sempre valide o corpo cru, antes de qualquer middleware encostar nele.
Quantas vezes a HubSpot refaz um webhook que falhou?
Até 10 vezes ao longo de 24 horas, disparadas por falhas de conexão, timeouts e qualquer resposta 4xx ou 5xx. O cronograma não é configurável, não há replay manual e, depois que a janela expira, o evento não é entregue de novo, e é por isso que persistir os eventos na chegada e ter uma rotina de backfill importa. Não confunda com a ação de webhook de workflows, que é outro sistema: ela tenta por até três dias e não refaz em respostas 4xx, exceto 429.
Os webhooks da HubSpot chegam em ordem?
Não. A entrega é at-least-once, sem garantia de ordem, e uma única requisição pode agrupar até 100 eventos de tipos misturados. Use o timestamp occurredAt de cada evento para sequenciar as mudanças e deduplique usando eventId mais portalId, porque o mesmo evento pode legitimamente chegar mais de uma vez.
Por que a validação de assinatura do meu webhook falha de forma intermitente?
Quase sempre porque algo modificou o corpo entre a HubSpot e a sua validação: re-serialização de JSON, trim de strings, normalização de espaços ou mudanças de encoding por middleware. Valide contra os bytes crus da requisição, reconstrua a URI exatamente como configurada (cuidado com proxies mudando scheme ou host) e lembre que o cabeçalho de timestamp é em milissegundos.
A HubSpot tem fila de dead-letter para webhooks?
Não. Depois dos 10 retries em 24 horas, as entregas falhas se vão, sem mecanismo de replay do lado da HubSpot. Integrações de produção persistem todo evento antes de processar, rodam sua própria fila de dead-letter para falhas de processamento e agendam backfills de reconciliação via Search API para curar as lacunas de quedas prolongadas.
Pronto para levar sua operação ao próximo nível.
Fale com um especialista e descubra como podemos ajudar.