最終更新日: 2025年8月22日
連携においてHubSpotから受け取るリクエストが実際にHubSpotから送信されたことを確かめるために、いくつかのヘッダーがリクエストに入力されます。これらのヘッダーと一緒に受信リクエストのフィールドを使用して、リクエストの署名を確認することができます。 署名の確認方法は署名のバージョンによって異なります。
  • HubSpot署名の最新バージョンを使用したリクエストを検証するには、X-HubSpot-Signature-V3ヘッダーを使用し、v3バージョンの署名を検証するための関連手順に従います。
  • 後方互換性を維持するために、HubSpotからのリクエストには旧バージョンの署名も含まれています。旧バージョンの署名を検証するには、X-HubSpot-Signature-Versionヘッダーをチェックしてから、バージョンがv1v2のどちらであるかに基づいて、以下の関連手順に従います。
以下の方法で、アプリのクライアントシークレットからのハッシュ値と受信リクエストのフィールドを抽出することができます。ハッシュ値を計算したら、その値を署名と比較します。両者が等しい場合は、リクエストは検証に合格したことになります。そうでない場合は、リクエストが転送中に改ざんされたか、あるいは他の誰かがエンドポイント宛ての偽装のリクエストを送信している可能性があります。

V1リクエスト署名を使用したリクエストの検証

アプリがWebhooks API経由でCRMオブジェクトイベントに登録されている場合は、X-HubSpot-Signature-Versionヘッダーがv1に設定されたリクエストがHubSpotから送信されます。X-HubSpot-Signatureヘッダーは、貴社のアプリのクライアントシークレットとリクエスト詳細の組み合わせに基づくSHA-256ハッシュになっています。 この署名のバージョンを確認するには、以下の手順に従います。
  • 次のように連結した文字列を作成します:Client secret + request body(存在する場合)。
  • 連結した文字列のSHA-256ハッシュを生成します。
  • ハッシュ値とX-HubSpot-Signatureヘッダーの値を比較します。
    • 同一の場合、このリクエストが検証に合格したことになります。
    • これらの値が一致しない場合、転送中にリクエストが改ざんされたか、誰かがHubSpotになりすまして貴社のエンドポイントにリクエストを送信していると考えられます。
リクエスト本文の例:
//Client secret : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
// Request body: [
{"eventId":1,"subscriptionId":12345,"
portalId":62515",
occurredAt":1564113600000",
subscriptionType":"contact.creation",
"attemptNumber":0,
"objectId":123,
"changeSource":"CRM",
"changeFlag":"NEW",
"appId":54321}
]

v1のリクエスト署名の例:

Pythonの例

NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.

>>> import hashlib

>>> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
>>> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
>>> source_string = client_secret + request_body
>>> source_string
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
>>> hashlib.sha256(source_string).hexdigest()
'232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de'

Ruby の例

NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.

irb(main):003:0> require 'digest'
=> true
irb(main):004:0> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
=> "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
irb(main):005:0> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
=> "[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]"
irb(main):006:0> source_string = client_secret + request_body
=> "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]"
irb(main):007:0> Digest::SHA256.hexdigest source_string
=> "232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de"

Node.jsの例

NOTE: This is only an example for generating the expected hash.
You will need to compare this expected hash with the actual hash in the
X-HubSpot-Signature header.

> const crypto = require('crypto')
undefined
> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
> request_body = '[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
'[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
> source_string = client_secret + request_body
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy[{"eventId":1,"subscriptionId":12345,"portalId":62515,"occurredAt":1564113600000,"subscriptionType":"contact.creation","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"NEW","appId":54321}]'
> hash = crypto.createHash('sha256').update(source_string).digest('hex')
'232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de'

Javaの例

// NOTE: This is only an example for generating the expected hash.
// You will need to compare this expected hash with the actual hash in the
// X-HubSpot-Signature header.

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.UnsupportedEncodingException;

public class HubSpotSignatureValidator {

    public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        String clientSecret = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
        String requestBody = "[{\"eventId\":1,\"subscriptionId\":12345,\"portalId\":62515,\"occurredAt\":1564113600000,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":123,\"changeSource\":\"CRM\",\"changeFlag\":\"NEW\",\"appId\":54321}]";

        String sourceString = clientSecret + requestBody;
        System.out.println("Source string: " + sourceString);

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(sourceString.getBytes("UTF-8"));

        // Convert to hex string (Java 17+)
        String hexString = java.util.HexFormat.of().formatHex(hash);

        System.out.println("Hash: " + hexString);
        // Output: 232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de
    }
}
この場合は以下のハッシュ値が得られます。232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de

V2リクエスト署名を使用したリクエストの検証

アプリがワークフロー内のWebhookアクションからのデータを処理している場合、またはカスタムCRMカード用のデータを返している場合は、X-HubSpot-Signature-Versionヘッダーがv2に設定されたリクエストがHubSpotから送信されます。X-HubSpot-Signatureヘッダーは、貴社のアプリのクライアントシークレットとリクエスト詳細の組み合わせに基づくSHA-256ハッシュになっています。 この署名を検証するには、次の手順に従います。
  • 次のように連結した文字列を作成します:Client secret + http method + URI + request body(存在する場合)。
  • 連結した文字列のSHA-256ハッシュを生成します。
  • ハッシュ値を署名と比較します。
    • 同一の場合、このリクエストが検証に合格したことになります。
    • これらの値が一致しない場合、転送中にリクエストが改ざんされたか、誰かがHubSpotになりすまして貴社のエンドポイントにリクエストを送信していると考えられます。
メモ:
  • ソース文字列の作成に使用するURIは、(プロトコルを含めて)元のリクエストと完全に一致する必要があります。署名の検証に問題がある場合は、クエリーパラメーターが元のリクエストに記載した順序と同じになっていることを確かめてください。
  • ソース文字列は、SHA-256ハッシュ値の計算前にUTF-8にエンコードしておく必要があります。

GETリクエストの例

GETリクエストでは、アプリのクライアントシークレットと、リクエストのメタデータの特定のフィールドが必要です。これらのフィールドとプレースホルダー値を以下に示します。
  • クライアントシークレット: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • HTTPメソッド: GET
  • URI: https://www.example.com/webhook_uri
  • リクエスト本文:""
連結された文字列は次のようになります。yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri 上記の連結文字列のSHA-256ハッシュを計算することで生成される、ヘッダーの署名と照合する署名は次のようになります。eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e

POSTリクエストの例

POSTリクエストでは、アプリのクライアントシークレット、リクエストのメタデータの特定のフィールド、リクエスト本文の文字列表現(例:Node.jsサービスではJSON.stringify(request.body)を使用)が必要です。これらのフィールドとプレースホルダー値を以下に示します。
  • クライアントシークレット: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • HTTPメソッド: POST
  • URI: https://www.example.com/webhook_uri
  • リクエスト本文: {"example_field":"example_value"}
連結された文字列は次のようになります。yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyPOSThttps://www.example.com/webhook_uri{"example_field":"example_value"} 上記の連結文字列のSHA-256ハッシュを計算することで生成される、ヘッダーの署名と照合する署名は次のようになります。9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900 署名の[SHAハッシュ値]を計算したら、その結果として得られる署名を、リクエストのx-hubspot-signatureヘッダーで提供される署名と比較できます。 以下のコードスニペットは、GETリクエストを処理するExpressサーバーを実行している場合に、v2リクエストの検証を組み込む方法について詳しく説明しています。以下のコードブロックは一例であり、Expressサービスの全機能を実行するために必要な特定の依存関係が省略されていることに注意してください。特定のサービスにリクエストの検証を実装するときには、最新の安定した安全なライブラリーを実行していることを確認してください。

v2のリクエスト署名の例

Node.jsの例

// Introduce any dependencies. Only several dependencies related to this example are included below:
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();

// Add any custom handling or setup code for your Node.js service here.
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// Example Node.js request validation code.
app.get('/example-service', (request, response, next) => {
  const { url, method, headers, hostname } = request;

  const requestSignature = headers['x-hubspot-signature'];

  // Compute expected signature
  const uri = `https://${hostname}${url}`;
  const encodedString = Buffer.from(
    `${process.env.CLIENT_SECRET}${method}${uri}`,
    'ascii'
  ).toString('utf-8');
  const expectedSignature = crypto
    .createHash('sha256')
    .update(encodedString)
    .digest('hex');

  console.log('Expected signature: %s', requestSignature);
  console.log('Request signature: %s', expectedSignature);

  // Add your custom handling to compare request signature to expected signature
  if (requestSignature !== expectedSignature) {
    console.log('Request of signature does NOT match!');
    response.status(400).send('Bad request');
  } else {
    console.log('Request of signature matches!');
    response.status(200).send();
  }
});

Javaの例

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

public class HubSpotV2SignatureValidator {

    public static boolean validateV2Signature(String clientSecret, String method,
                                                String uri,
                                                String receivedSignature) {
        try {
          // Create concatenated string: client_secret + method + uri + body
          String sourceString = clientSecret + method + uri;
          System.out.println("Source string: " + sourceString);

          // Create SHA-256 hash
          MessageDigest digest = MessageDigest.getInstance("SHA-256");
          byte[] hash = digest.digest(sourceString.getBytes(StandardCharsets.UTF_8));

          // Convert to hex string (Java 17+)
          String expectedSignature = java.util.HexFormat.of().formatHex(hash);

          // Compare signatures using constant-time comparison
          return MessageDigest.isEqual(expectedSignature.getBytes(), receivedSignature.getBytes());

        } catch (NoSuchAlgorithmException e) {
          throw new RuntimeException("SHA-256 algorithm not available", e);
        }
      }

      // Example usage
      public static void main(String[] args) {
        String clientSecret = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
        String method = "GET";
        String uri = "https://www.example.com/webhook_uri";

        // Expected signature: 9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900
        String expectedSignature = "9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900";

        boolean isValid = validateV2Signature(clientSecret, method, uri, expectedSignature);
        if (isValid) {
          System.out.println("Signature is valid!");
        }
        // Proceed with any request processing as needed.
       else {
        System.out.println("Signature is invalid!");
        // Add any rejection logic here. e.g, throw 400
        }
      }
}

V3のリクエスト署名の検証

X-HubSpot-Signature-v3ヘッダーは、貴社のアプリのクライアントシークレットとリクエスト詳細の組み合わせに基づくHMAC SHA-256ハッシュになっています。また、X-HubSpot-Request-Timestampヘッダーも含まれます。 以下の手順に従って、X-HubSpot-Signature-v3ヘッダーを使用してリクエストを検証します。
  • タイムスタンプが5分前よりも古い場合には、リクエストを拒否します。
  • リクエストURIで、下表に示すURLエンコード文字のいずれかをデコードします。クエリー文字列の先頭を示す疑問符をデコードする必要はありません。
エンコード値デコード値
%3A:
%2F/
%3F?
%40@
%21!
%24$
%27'
%28(
%29)
%2A*
%2C,
%3B;
  • 次のように連結したUTF-8でエンコードされた文字列を作成します:requestMethod + requestUri + requestBody +タイムスタンプ。タイムスタンプは、X-HubSpot-Request-Timestampヘッダーで提供されます。
  • アプリシークレットをHMAC SHA-256関数のシークレットとして使用し、連結した文字列のHMAC SHA-256ハッシュを作成します。
  • HMAC関数の結果をBase64エンコードします。
  • ハッシュ値を署名と比較します。同一の場合、このリクエストの発信元がHubSpotであることが検証されたことになります。タイミング攻撃を防ぐために、一定時間の文字列比較を使用することをお勧めします。
以下のセクションにあるコードスニペットは、受信リクエストを処理するバックエンドサービスを実行している場合に、POSTリクエストにv3リクエスト検証を組み込む方法を詳しく説明しています。以下のコードブロックは、フル機能のバックエンドサービスを実行するために必要な特定の依存関係を省略している点に留意してください。特定のサービスにリクエストの検証を実装するときには、最新の安定した安全なライブラリーを実行していることを確認してください。

v3のリクエスト署名の例:

Node.jsの例

// Introduce any dependencies. Only several dependencies related to this example are included below:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const port = process.env.PORT || 4000;

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.post('/webhook-test', (request, response) => {
  response.status(200).send('Received webhook subscription trigger');

  const { url, method, body, headers, hostname } = request;

  // Parse headers needed to validate signature
  const signatureHeader = headers['x-hubspot-signature-v3'];
  const timestampHeader = headers['x-hubspot-request-timestamp'];

  // Validate timestamp
  const MAX_ALLOWED_TIMESTAMP = 300000; // 5 minutes in milliseconds
  const currentTime = Date.now();
  if (currentTime - timestampHeader > MAX_ALLOWED_TIMESTAMP) {
    console.log('Timestamp is invalid, reject request');
    // Add any rejection logic here
  }

  // Concatenate request method, URI, body, and header timestamp
  const uri = `https://${hostname}${url}`;
  const rawString = `${method}${uri}${JSON.stringify(body)}${timestampHeader}`;

  // Create HMAC SHA-256 hash from resulting string above, then base64-encode it
  const hashedString = crypto
    .createHmac('sha256', process.env.CLIENT_SECRET)
    .update(rawString)
    .digest('base64');

  // Validate signature: compare computed signature vs. signature in header
  if (
    crypto.timingSafeEqual(
      Buffer.from(hashedString),
      Buffer.from(signatureHeader)
    )
  ) {
    console.log('Signature matches! Request is valid.');
    // Proceed with any request processing as needed.
  } else {
    console.log('Signature does not match: request is invalid');
    // Add any rejection logic here.
  }
});

Javaの例

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class HubSpotV3SignatureValidator {

    // 5 minutes in milliseconds
    private static final long MAX_ALLOWED_TIMESTAMP = 300000;

      public static boolean validateV3Signature(String clientSecret, String method,
                                                String uri, String requestBody,
                                                long timestamp, String receivedSignature) {
        try {
          // Validate timestamp (reject if older than 5 minutes)
          long currentTime = System.currentTimeMillis();
          if (currentTime - timestamp > MAX_ALLOWED_TIMESTAMP) {
            System.out.println("Timestamp is invalid, rejecting request");
            return false;
          }

          // Create concatenated string: method + uri + body + timestamp
          String rawString = method + uri + requestBody + timestamp;
          System.out.println("Raw string: " + rawString);

          // Create HMAC SHA-256 hash
          Mac hmacSha256 = Mac.getInstance("HmacSHA256");
          SecretKeySpec secretKey = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
          hmacSha256.init(secretKey);

          byte[] hash = hmacSha256.doFinal(rawString.getBytes(StandardCharsets.UTF_8));

          // Base64 encode the result
          String expectedSignature = Base64.getEncoder().encodeToString(hash);

          // Compare signatures using constant-time comparison
          return MessageDigest.isEqual(expectedSignature.getBytes(), receivedSignature.getBytes());

        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
          throw new RuntimeException("Error creating HMAC SHA-256 hash", e);
        }
      }

      // Example usage
      public static void main(String[] args) {
        String clientSecret = "cfc68c0b-4b4e-4ef8-b764-95350e4ea479";
        String method = "POST";
        String uri = "https://webhook.site/335453f5-94b3-49d9-b684-a55354d4b8df";
        String requestBody = "[{\"eventId\":531833541,\"subscriptionId\":3923621,\"portalId\":48807704,\"appId\":16111050,\"occurredAt\":1752613920733,\"subscriptionType\":\"contact.creation\",\"attemptNumber\":0,\"objectId\":138017612137,\"changeFlag\":\"CREATED\",\"changeSource\":\"CRM_UI\",\"sourceId\":\"userId:76023669\"}]";
        long timestamp = 1752613922216L; // Example timestamp in milliseconds

        // This would typically come from the X-HubSpot-Signature-v3 header
        String signatureFromHeader = "gbj1XPRvUt0noT7i7fXfTzOD4sLzQmf0VT28ZYq0EYg=";

        boolean isValid = validateV3Signature(clientSecret, method, uri, requestBody, timestamp, signatureFromHeader);
        if(isValid) {
          System.out.println("Signature is valid! Proceed with request processing.");
        }
        // Proceed with any request processing as needed.
       else {
        System.out.println("Signature is invalid! Reject the request.");
        // Add any rejection logic here, e.g., throw 400 Bad Request
        }
      }
}