Tous les articles
Tester les webhooks HubSpot sur localhost en toute sécurité
HubSpotwebhookslocalhostCRM integrations

Tester les webhooks HubSpot sur localhost en toute sécurité

Un service distant ne peut pas joindre localhost. Le tunnel fournit une URL HTTPS publique et transmet chaque requête au serveur local, avec les vrais headers et le body original. Conservez le body brut avant tout parseur JSON. Vérifiez la signature, validez les champs attendus, enregistrez la livraison durablement, puis seulement lancez le traitement métier.

Comprendre le flux HubSpot vers une application locale

Un service distant ne peut pas joindre localhost. Le tunnel fournit une URL HTTPS publique et transmet chaque requête au serveur local, avec les vrais headers et le body original. >documentation officielle

1. Créer un endpoint qui conserve le body brut

Conservez le body brut avant tout parseur JSON. Vérifiez la signature, validez les champs attendus, enregistrez la livraison durablement, puis seulement lancez le traitement métier.

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);

Enregistrez l’URL HTTPS complète, limitez l’abonnement aux événements utiles et déclenchez un cas réel dans un environnement de test. Stockez le secret hors du dépôt.

2. Exposer le port local en HTTPS

Démarrez le serveur, ouvrez le tunnel dans un second terminal et gardez le processus actif pendant le test. Une réponse 401 à une requête non signée confirme que le routage fonctionne.

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

Démarrez le serveur, ouvrez le tunnel dans un second terminal et gardez le processus actif pendant le test. Une réponse 401 à une requête non signée confirme que le routage fonctionne.

3. Configurer le webhook dans HubSpot

Enregistrez l’URL HTTPS complète, limitez l’abonnement aux événements utiles et déclenchez un cas réel dans un environnement de test. Stockez le secret hors du dépôt. >documentation officielle

Traitez uniquement les types connus, acceptez les champs additionnels et utilisez un identifiant de livraison stable pour la déduplication.

Vérifier la signature sur les octets exacts

Calculez le HMAC avec le secret du webhook et les données exactes définies par le fournisseur. Utilisez une comparaison en temps constant et ne journalisez jamais le secret. >documentation officielle

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);
}

Contrôlez l’horodatage après la signature et rejetez les messages trop anciens. Une horloge système synchronisée évite les faux rejets.

Calculez le HMAC avec le secret du webhook et les données exactes définies par le fournisseur. Utilisez une comparaison en temps constant et ne journalisez jamais le secret. >guide pratique

Répondre vite et traiter de façon idempotente

Traitez uniquement les types connus, acceptez les champs additionnels et utilisez un identifiant de livraison stable pour la déduplication.

Réservez l’identifiant et créez le job dans une transaction atomique avant de répondre 200. Retournez une erreur si le stockage durable est indisponible. >guide pratique

Diagnostiquer les échecs de livraison HubSpot

  • Démarrez le serveur, ouvrez le tunnel dans un second terminal et gardez le processus actif pendant le test. Une réponse 401 à une requête non signée confirme que le routage fonctionne.
  • Calculez le HMAC avec le secret du webhook et les données exactes définies par le fournisseur. Utilisez une comparaison en temps constant et ne journalisez jamais le secret.
  • Vérifiez le routage, les headers, la signature, le délai de réponse et l’idempotence. Les requêtes synthétiques servent surtout à tester les chemins de rejet.
  • En cas d’échec, contrôlez le chemin POST, le port du tunnel, le body brut, le secret, l’horloge et la latence de la base de données.

>guide pratique · >guide pratique

Checklist de sécurité pour le développement et la production

  • Exigez HTTPS, limitez taille et méthode, protégez les secrets, masquez les données sensibles dans les logs et supprimez les URLs de test obsolètes.
  • Conservez le body brut avant tout parseur JSON. Vérifiez la signature, validez les champs attendus, enregistrez la livraison durablement, puis seulement lancez le traitement métier.
  • Réservez l’identifiant et créez le job dans une transaction atomique avant de répondre 200. Retournez une erreur si le stockage durable est indisponible.

Exigez HTTPS, limitez taille et méthode, protégez les secrets, masquez les données sensibles dans les logs et supprimez les URLs de test obsolètes.

Questions fréquentes

Comment tester un webhook HubSpot sur localhost ?
Démarrez le serveur, ouvrez le tunnel dans un second terminal et gardez le processus actif pendant le test. Une réponse 401 à une requête non signée confirme que le routage fonctionne.
Comment vérifier la signature d’un webhook HubSpot ?
Calculez le HMAC avec le secret du webhook et les données exactes définies par le fournisseur. Utilisez une comparaison en temps constant et ne journalisez jamais le secret.
Pourquoi HubSpot renvoie-t-il le webhook ?
Réservez l’identifiant et créez le job dans une transaction atomique avant de répondre 200. Retournez une erreur si le stockage durable est indisponible.
Quelle clé utiliser pour assurer l’idempotence ?
Traitez uniquement les types connus, acceptez les champs additionnels et utilisez un identifiant de livraison stable pour la déduplication.