Todos os artigos
Como testar webhooks do HubSpot no localhost com segurança
HubSpotwebhookslocalhostCRM integrations

Como testar webhooks do HubSpot no localhost com segurança

Um serviço remoto não consegue acessar localhost diretamente. O túnel oferece uma URL HTTPS pública e encaminha os headers reais e o body original ao servidor local. Preserve o raw body antes do JSON parser. Valide assinatura e dados, grave a entrega de forma durável e só então execute a lógica de negócio.

Como o webhook do HubSpot chega a uma aplicação local

Um serviço remoto não consegue acessar localhost diretamente. O túnel oferece uma URL HTTPS pública e encaminha os headers reais e o body original ao servidor local. >documentação oficial

1. Crie um endpoint que preserve o raw body

Preserve o raw body antes do JSON parser. Valide assinatura e dados, grave a entrega de forma durável e só então execute a lógica de negócio.

import express from "express";
import crypto from "node:crypto";

const app = express();
const secret = process.env.HUBSPOT_CLIENT_SECRET;
const publicBase = process.env.WEBHOOK_PUBLIC_BASE_URL;

app.post(
  "/webhooks/hubspot",
  express.raw({ type: "application/json", limit: "1mb" }),
  async (req, res) => {
    const signature = req.get("x-hubspot-signature-v3");
    const timestamp = req.get("x-hubspot-request-timestamp");
    const rawBody = req.body.toString("utf8");

    if (!verifyHubSpotV3({
      signature,
      timestamp,
      method: req.method,
      publicUri: `${publicBase}${req.originalUrl}`,
      rawBody,
      secret,
    })) {
      return res.sendStatus(401);
    }

    let events;
    try {
      events = JSON.parse(rawBody);
      if (!Array.isArray(events)) throw new Error("Expected a batch");
      await enqueueBatchIdempotently(events);
    } catch (error) {
      console.error("HubSpot webhook rejected", error);
      return res.sendStatus(500);
    }

    return res.sendStatus(200);
  }
);

app.use(express.json());
app.listen(3000);

Cadastre a URL HTTPS completa, assine apenas os eventos necessários e gere um caso real no ambiente de teste. Guarde o secret fora do repositório.

2. Exponha a porta local por HTTPS

Inicie o servidor e mantenha o túnel aberto em outro terminal. Um 401 para uma requisição sem assinatura confirma que o routing funciona e a autenticação rejeita corretamente.

npx portpreview 3000
https://example.portpreview.dev/webhooks/hubspot

Inicie o servidor e mantenha o túnel aberto em outro terminal. Um 401 para uma requisição sem assinatura confirma que o routing funciona e a autenticação rejeita corretamente.

3. Configure o webhook no HubSpot

Cadastre a URL HTTPS completa, assine apenas os eventos necessários e gere um caso real no ambiente de teste. Guarde o secret fora do repositório. >documentação oficial

Processe apenas type conhecidos, aceite campos adicionais e elimine duplicidades com um ID de entrega estável em armazenamento durável.

Valide a assinatura sobre os bytes exatos

Calcule o HMAC com o webhook secret e os dados exatos definidos pelo provedor. Compare em tempo constante e nunca registre o secret. >documentação oficial

function decodeHubSpotQuery(uri) {
  const [base, query] = uri.split("?", 2);
  if (query === undefined) return base;
  const map = {
    "%3A": ":", "%2F": "/", "%3F": "?", "%40": "@",
    "%21": "!", "%24": "$", "%27": "'", "%28": "(",
    "%29": ")", "%2A": "*", "%2C": ",", "%3B": ";",
  };
  const decoded = query.replace(
    /%3A|%2F|%3F|%40|%21|%24|%27|%28|%29|%2A|%2C|%3B/g,
    value => map[value]
  );
  return `${base}?${decoded}`;
}

function verifyHubSpotV3(input) {
  if (!input.secret || !input.signature || !input.timestamp) return false;

  const sentAt = Number(input.timestamp);
  if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > 300_000) {
    return false;
  }

  const uri = decodeHubSpotQuery(input.publicUri.split("#")[0]);
  const source = `${input.method}${uri}${input.rawBody}${input.timestamp}`;
  const expected = crypto
    .createHmac("sha256", input.secret)
    .update(source, "utf8")
    .digest("base64");

  const actualBuffer = Buffer.from(input.signature, "utf8");
  const expectedBuffer = Buffer.from(expected, "utf8");
  return actualBuffer.length === expectedBuffer.length &&
    crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}

Confira a validade do timestamp depois da assinatura e mantenha o relógio do host sincronizado.

Calcule o HMAC com o webhook secret e os dados exatos definidos pelo provedor. Compare em tempo constante e nunca registre o secret. >guia prático

Responda rápido e processe com idempotência

Processe apenas type conhecidos, aceite campos adicionais e elimine duplicidades com um ID de entrega estável em armazenamento durável.

Reserve o ID e crie o job na mesma transaction antes de responder 200. Se a persistência falhar, retorne erro para permitir retry. >guia prático

Resolva falhas do webhook do HubSpot

  • Inicie o servidor e mantenha o túnel aberto em outro terminal. Um 401 para uma requisição sem assinatura confirma que o routing funciona e a autenticação rejeita corretamente.
  • Calcule o HMAC com o webhook secret e os dados exatos definidos pelo provedor. Compare em tempo constante e nunca registre o secret.
  • Confira routing, headers, assinatura, tempo de resposta e idempotência. Use requisições sintéticas principalmente para testar rejeições.
  • Revise o POST path, a port do túnel, o raw body, o secret, o relógio e a latência do banco de dados.

>guia prático · >guia prático

Checklist de segurança para desenvolvimento e produção

  • Exija HTTPS, limite tamanho e method, proteja secrets, remova dados sensíveis dos logs e exclua URLs de teste antigas.
  • Preserve o raw body antes do JSON parser. Valide assinatura e dados, grave a entrega de forma durável e só então execute a lógica de negócio.
  • Reserve o ID e crie o job na mesma transaction antes de responder 200. Se a persistência falhar, retorne erro para permitir retry.

Exija HTTPS, limite tamanho e method, proteja secrets, remova dados sensíveis dos logs e exclua URLs de teste antigas.

Perguntas frequentes

Como testar um webhook do HubSpot no localhost?
Inicie o servidor e mantenha o túnel aberto em outro terminal. Um 401 para uma requisição sem assinatura confirma que o routing funciona e a autenticação rejeita corretamente.
Como validar a assinatura do webhook do HubSpot?
Calcule o HMAC com o webhook secret e os dados exatos definidos pelo provedor. Compare em tempo constante e nunca registre o secret.
Por que o HubSpot tenta entregar o webhook novamente?
Reserve o ID e crie o job na mesma transaction antes de responder 200. Se a persistência falhar, retorne erro para permitir retry.
Qual chave usar para idempotência?
Processe apenas type conhecidos, aceite campos adicionais e elimine duplicidades com um ID de entrega estável em armazenamento durável.