Усі статті
Безпечне тестування webhook HubSpot на localhost
HubSpotwebhookslocalhostCRM integrations

Безпечне тестування webhook HubSpot на localhost

Зовнішній сервіс не може напряму звернутися до localhost. Тунель надає публічний HTTPS URL і передає локальному серверу справжні headers та незмінений body. Збережіть raw body до JSON parser. Спочатку перевірте підпис і дані, надійно запишіть доставку й лише потім запускайте бізнес-логіку.

Як webhook HubSpot потрапляє до локального застосунку

Зовнішній сервіс не може напряму звернутися до localhost. Тунель надає публічний HTTPS URL і передає локальному серверу справжні headers та незмінений body. >офіційна документація

1. Створіть endpoint зі збереженням raw body

Збережіть raw body до JSON parser. Спочатку перевірте підпис і дані, надійно запишіть доставку й лише потім запускайте бізнес-логіку.

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

Укажіть повний HTTPS URL, підпишіться лише на потрібні події та виконайте реальний сценарій у тестовому середовищі. Зберігайте secret поза репозиторієм.

2. Відкрийте локальний порт через HTTPS

Запустіть сервер і тримайте тунель відкритим у другому терміналі. Відповідь 401 на запит без підпису підтверджує правильний routing.

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

Запустіть сервер і тримайте тунель відкритим у другому терміналі. Відповідь 401 на запит без підпису підтверджує правильний routing.

3. Налаштуйте webhook у HubSpot

Укажіть повний HTTPS URL, підпишіться лише на потрібні події та виконайте реальний сценарій у тестовому середовищі. Зберігайте secret поза репозиторієм. >офіційна документація

Обробляйте відомі type, приймайте додаткові поля та усувайте дублікати за стабільним ID доставки.

Перевірте підпис за точними байтами

Обчисліть HMAC із webhook secret і точними даними зі специфікації. Порівнюйте за сталий час і не записуйте secret у логи. >офіційна документація

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

Після перевірки підпису контролюйте свіжість timestamp і синхронізуйте годинник сервера.

Обчисліть HMAC із webhook secret і точними даними зі специфікації. Порівнюйте за сталий час і не записуйте secret у логи. >практичний посібник

Відповідайте швидко й обробляйте ідемпотентно

Обробляйте відомі type, приймайте додаткові поля та усувайте дублікати за стабільним ID доставки.

Резервуйте ID доставки та створюйте job в одній transaction до відповіді 200. Якщо storage недоступний, поверніть помилку для retry. >практичний посібник

Діагностика помилок webhook HubSpot

  • Запустіть сервер і тримайте тунель відкритим у другому терміналі. Відповідь 401 на запит без підпису підтверджує правильний routing.
  • Обчисліть HMAC із webhook secret і точними даними зі специфікації. Порівнюйте за сталий час і не записуйте secret у логи.
  • Перевірте routing, headers, підпис, час відповіді та ідемпотентність. Синтетичні запити корисні для сценаріїв відмови.
  • Перевірте POST path, port тунелю, raw body, secret, годинник і затримку бази даних.

>практичний посібник · >практичний посібник

Перевірка безпеки для розробки та production

  • Вимагайте HTTPS, обмежте розмір і method, захищайте secrets, очищуйте логи та видаляйте застарілі тестові URL.
  • Збережіть raw body до JSON parser. Спочатку перевірте підпис і дані, надійно запишіть доставку й лише потім запускайте бізнес-логіку.
  • Резервуйте ID доставки та створюйте job в одній transaction до відповіді 200. Якщо storage недоступний, поверніть помилку для retry.

Вимагайте HTTPS, обмежте розмір і method, захищайте secrets, очищуйте логи та видаляйте застарілі тестові URL.

Поширені запитання

Як протестувати webhook HubSpot на localhost?
Запустіть сервер і тримайте тунель відкритим у другому терміналі. Відповідь 401 на запит без підпису підтверджує правильний routing.
Як перевірити підпис webhook HubSpot?
Обчисліть HMAC із webhook secret і точними даними зі специфікації. Порівнюйте за сталий час і не записуйте secret у логи.
Чому HubSpot повторює доставку webhook?
Резервуйте ID доставки та створюйте job в одній transaction до відповіді 200. Якщо storage недоступний, поверніть помилку для retry.
Який ключ використовувати для ідемпотентності?
Обробляйте відомі type, приймайте додаткові поля та усувайте дублікати за стабільним ID доставки.