Alle Artikel
HubSpot-Webhooks sicher auf localhost testen
HubSpotwebhookslocalhostCRM integrations

HubSpot-Webhooks sicher auf localhost testen

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.

Häufig gestellte Fragen

Wie teste ich einen HubSpot-Webhook auf localhost?
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.
Wie prüfe ich die Signatur eines HubSpot-Webhooks?
HMAC mit dem Webhook-secret und den exakt spezifizierten Daten berechnen. In konstanter Zeit vergleichen und das secret nie protokollieren.
Warum stellt HubSpot den Webhook erneut zu?
Zustell-ID und job in einer atomaren Transaktion anlegen, bevor 200 zurückgegeben wird. Bei ausgefallenem Storage einen Fehler liefern.
Welcher Schlüssel eignet sich für Idempotenz?
Nur bekannte type-Werte verarbeiten, zusätzliche Felder tolerieren und mit einer stabilen Zustell-ID dauerhaft deduplizieren.