HubSpot の Webhook 配信の仕組み

5 秒の応答期限が、最大 100 件の通知を含むバッチ全体にかかります。eventId は一意とは保証されていないと明記されています。v3 署名と Node の検証コードを掲載しています。

配信の挙動

プロバイダーの公式ドキュメントから読み取っています(最終確認 2026-08-06)。プロバイダーが数値を示していない項目には、この表も数値を載せていません。

応答期限5 秒

最大 100 件の通知を含むバッチ全体に適用される

ハンドラーが期限内に応答するか確認する

リトライ24 時間にわたり最大 10 回試行
打ち切りプロバイダーは未公表
出典https://developers.hubspot.com/docs/api-reference/latest/webhooks/guide

署名

ドキュメントを読んだだけではなく、テストスイートで裏付けた動作する検証コードで確認しています。最終検証日は 2026-08-20 です。

アルゴリズムHMAC-SHA256
署名対象POST{decodedUrl}{body}{timestamp}
エンコードbase64
ヘッダーx-hubspot-signature-v3
許容範囲5 分

検証する

JSON としてパースする前の生のリクエストボディを、バイト単位でそのまま使います。フレームワークがペイロードを再シリアライズした時点で、このページのどの方式も検証に失敗します。

const crypto = require("node:crypto");

// v3: base64(HMAC-SHA256(secret, "POST" + url + body + timestamp))
const ts = req.headers["x-hubspot-request-timestamp"];
if (Math.abs(Date.now() - Number(ts)) > 300_000) throw new Error("stale");

const expected = crypto
  .createHmac("sha256", clientSecret) // the app's client secret
  .update("POST" + fullUrl + rawBody + ts)
  .digest("base64");
const received = Buffer.from(req.headers["x-hubspot-signature-v3"] ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
  received.length === wanted.length && crypto.timingSafeEqual(received, wanted);

fullUrl は HubSpot が呼び出した公開 URL そのもので、クエリ文字列を含み、URL デコードした後の値です。Host ヘッダーを書き換えるプロキシの後ろにある場合は、転送されたヘッダーから URL を組み立て直してください。そうしないと署名は決して一致しません。

落とし穴

5 秒の応答期限はリクエスト単位で、リクエストには最大 100 件の通知がバッチで入ることがあります。その場で処理するなら、実際に使えるのは通知ごとに 50ms です。

HubSpot は eventId の一意性を保証しないと明記しており、順序も約束していません。そのため、イベント ID で重複排除するという Stripe での習慣はそのまま使えません。

あわせて読む

AnyHook は、HubSpot から受信するエンドポイントの手前に入ります。約 1 秒で応答し、サーバーがダウンしていれば独自のスケジュールでリトライします。すべてのイベントは再送できる状態で保存されます。 変えるのは URL 1 つだけで、コードはそのままです。ただし HubSpot 自身の署名は 5 分で失効するため、リトライと再送を受け付けるには AnyHook-Signature を検証してください。

仕組みを見る →