Ein externer Dienst kann localhost nicht direkt erreichen. Der Tunnel stellt eine öffentliche HTTPS-URL bereit und leitet echte headers sowie den unveränderten body an den lokalen Server weiter. Den raw body vor jedem JSON parser sichern. Erst Signatur und Eingaben prüfen, die Zustellung dauerhaft speichern und danach die Geschäftslogik starten.
Wie HubSpot-Webhooks eine lokale Anwendung erreichen
Ein externer Dienst kann localhost nicht direkt erreichen. Der Tunnel stellt eine öffentliche HTTPS-URL bereit und leitet echte headers sowie den unveränderten body an den lokalen Server weiter. >offizielle Dokumentation
1. Endpoint mit unverändertem raw body erstellen
Den raw body vor jedem JSON parser sichern. Erst Signatur und Eingaben prüfen, die Zustellung dauerhaft speichern und danach die Geschäftslogik starten.
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);
Die vollständige HTTPS-URL eintragen, nur benötigte Events abonnieren und einen echten Fall im Testsystem auslösen. Das secret gehört nicht ins Repository.
2. Lokalen Port über HTTPS bereitstellen
Server starten und den Tunnel in einem zweiten Terminal offen halten. Ein 401 für einen unsignierten Request bestätigt funktionierendes Routing und korrekte Ablehnung.
npx portpreview 3000
https://example.portpreview.dev/webhooks/hubspot
Server starten und den Tunnel in einem zweiten Terminal offen halten. Ein 401 für einen unsignierten Request bestätigt funktionierendes Routing und korrekte Ablehnung.
3. Webhook in HubSpot konfigurieren
Die vollständige HTTPS-URL eintragen, nur benötigte Events abonnieren und einen echten Fall im Testsystem auslösen. Das secret gehört nicht ins Repository. >offizielle Dokumentation
Nur bekannte type-Werte verarbeiten, zusätzliche Felder tolerieren und mit einer stabilen Zustell-ID dauerhaft deduplizieren.
Signatur über die exakten Bytes prüfen
HMAC mit dem Webhook-secret und den exakt spezifizierten Daten berechnen. In konstanter Zeit vergleichen und das secret nie protokollieren. >offizielle Dokumentation
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);
}
Nach erfolgreicher Signaturprüfung die Aktualität des timestamp prüfen und die Host-Uhr synchron halten.
HMAC mit dem Webhook-secret und den exakt spezifizierten Daten berechnen. In konstanter Zeit vergleichen und das secret nie protokollieren. >Praxisleitfaden
Schnell bestätigen und idempotent verarbeiten
Nur bekannte type-Werte verarbeiten, zusätzliche Felder tolerieren und mit einer stabilen Zustell-ID dauerhaft deduplizieren.
Zustell-ID und job in einer atomaren Transaktion anlegen, bevor 200 zurückgegeben wird. Bei ausgefallenem Storage einen Fehler liefern. >Praxisleitfaden
Fehler bei HubSpot-Webhooks beheben
- Server starten und den Tunnel in einem zweiten Terminal offen halten. Ein 401 für einen unsignierten Request bestätigt funktionierendes Routing und korrekte Ablehnung.
- HMAC mit dem Webhook-secret und den exakt spezifizierten Daten berechnen. In konstanter Zeit vergleichen und das secret nie protokollieren.
- Routing, headers, Signatur, Antwortzeit und Idempotenz prüfen. Synthetische Requests eignen sich vor allem für Ablehnungspfade.
- POST-path, Tunnel-port, raw body, secret, Systemzeit und Datenbanklatenz kontrollieren.
>Praxisleitfaden · >Praxisleitfaden
Sicherheitscheckliste für Entwicklung und Produktion
- HTTPS erzwingen, Größe und method begrenzen, secrets schützen, Logs bereinigen und veraltete Test-URLs entfernen.
- Den raw body vor jedem JSON parser sichern. Erst Signatur und Eingaben prüfen, die Zustellung dauerhaft speichern und danach die Geschäftslogik starten.
- Zustell-ID und job in einer atomaren Transaktion anlegen, bevor 200 zurückgegeben wird. Bei ausgefallenem Storage einen Fehler liefern.
HTTPS erzwingen, Größe und method begrenzen, secrets schützen, Logs bereinigen und veraltete Test-URLs entfernen.
