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.
