Dernière modification : 22 août 2025
Pour vérifier que les requêtes que votre intégration reçoit de HubSpot proviennent bien de HubSpot, plusieurs en-têtes sont renseignés dans la requête. Vous pouvez utiliser ces en-têtes ainsi que les champs de la requête entrante pour vérifier la signature de la requête. La méthode utilisée pour vérifier la signature dépend de la version de la signature :
  • Pour valider une requête à l’aide de la dernière version de la signature HubSpot, utilisez l’en-tête X-HubSpot-Signature-V3 et suivez les instructions associées pour valider la version 3 de la signature.
  • Pour la rétrocompatibilité, les requêtes de HubSpot incluent également les anciennes versions de la signature. Pour valider une ancienne version de la signature, consultez l’en-tête X-HubSpot-Signature-Version, puis suivez les instructions associées ci-dessous selon la version v1 ou v2.
Dans les instructions ci-dessous, découvrez comment obtenir une valeur de hachage à partir du secret client de votre application et des champs d’une requête entrante. Une fois que vous avez calculé la valeur de hachage, comparez-la à la signature. Si les deux sont égales, la requête a été validée avec succès. Autrement, cette requête peut avoir été modifiée en transit ou quelqu’un peut usurper les requêtes à votre point de terminaison.

Valider les requêtes à l’aide de la signature de requête v1

Si votre application est abonnée aux événements d’objet CRM via l’API des webhooks, les requêtes de HubSpot seront envoyées avec l’en-tête X-HubSpot-Signature-Version défini sur v1. L’en-tête X-HubSpot-Signature sera un hachage SHA-256 conçu à l’aide du secret de client de votre application associé à des détails de la requête. Pour vérifier la version de cette signature, effectuez les étapes suivantes :
  • Créez une chaîne qui regroupe les éléments suivants : Client secret + request body (le cas échéant)
  • Créez un hachage SHA-256 de la chaîne résultante
  • Comparez la valeur de hachage à la valeur de l’en-tête X-HubSpot-Signature :
    • Si elle est égale, cette requête a été validée
    • Si ces valeurs sont différentes, cette requête peut avoir été modifiée en transit ou quelqu’un peut usurper les requêtes à votre point de terminaison.
Exemple de requête avec un corps :
//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}
]

Exemples de signatures de requête v1 :

Exemple 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'

Exemple 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"

Exemple 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'

Exemple 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
    }
}
Le hachage résultant serait : 232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de

Valider les requêtes à l’aide de la signature de requête v2

Si votre application gère des données provenant d’une action de webhook dans un workflow ou si vous renvoyez des données pour une carte CRM personnalisée, la requête de HubSpot est envoyée avec l’en-tête X-HubSpot-Signature-Version défini sur v2. L’en-tête X-HubSpot-Signature sera un hachage SHA-256 conçu à l’aide du secret de client de votre application associé à des détails de la requête. Pour vérifier cette signature, effectuez les étapes suivantes :
  • Créez une chaîne qui regroupe les éléments suivants : Client secret + http method + URI + request body (le cas échéant)
  • Créez un hachage SHA-256 de la chaîne résultante
  • Comparez la valeur de hachage à la signature
    • Si elle est égale, cette requête a été validée
    • Si ces valeurs sont différentes, cette requête peut avoir été modifiée en transit ou quelqu’un peut usurper les requêtes à votre point de terminaison.
Remarques :
  • L’URI utilisé pour créer la chaîne source doit correspondre exactement à la requête initiale, y compris le protocole. Si vous rencontrez des difficultés lors de la validation de la signature, assurez-vous que tous les paramètres de requête sont dans le même ordre que dans la requête initiale.
  • La chaîne source doit être encodée en UTF-8 avant de calculer le hachage SHA-256.

Exemple pour une requête GET

Pour une requête GET, vous aurez besoin du secret client de votre application et des champs spécifiques des métadonnées de votre requête. Ces champs sont répertoriés ci-dessous avec des valeurs d’espace réservé incluses :
  • Secret du client : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • Méthode HTTP : GET
  • URI : https://www.example.com/webhook_uri
  • Corps de la requête : ""
La chaîne concaténée résultante serait : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri Après avoir calculé un hachage SHA-256 de la chaîne concaténée ci-dessus, la signature résultante que vous vous attendez à faire correspondre à celle de l’en-tête serait : eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e

Exemple pour une requête POST

Pour une requête POST, vous aurez besoin du secret client de votre application, de champs spécifiques des métadonnées de votre requête et d’une représentation sous forme de chaîne du corps de la requête (par exemple : utilisation de JSON.stringify(request.body) pour un service Node.js). Ces champs sont répertoriés ci-dessous avec des valeurs d’espace réservé incluses :
  • Secret du client : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • Méthode HTTP : POST
  • URI : https://www.example.com/webhook_uri
  • Corps de la requête : {"example_field":"example_value"}
La chaîne concaténée résultante serait : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyPOSThttps://www.example.com/webhook_uri{"example_field":"example_value"} Après avoir calculé un hachage SHA-256 de la chaîne concaténée ci-dessus, la signature résultante que vous vous attendez à faire correspondre à celle de l’en-tête serait : 9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900 Après [SHA-ing] la signature, vous pouvez comparer la signature attendue résultante à celle fournie dans l’en-tête x-hubspot-signature de la requête : Le bloc de code Node.js ci-dessous détaille comment vous pouvez incorporer la validation des requêtes v2 pour une requête GET si vous exécutiez un serveur Express pour gérer les requêtes entrantes. Gardez à l’esprit que le bloc de code ci-dessous est un exemple et omet certaines dépendances dont vous pourriez avoir besoin pour exécuter un service Express complet. Confirmez que vous utilisez les dernières bibliothèques stables et sécurisées lors de la mise en œuvre de la validation des requêtes pour votre service spécifique.

Exemples de signatures de requête v2 :

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

Exemple 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
        }
      }
}

Valider les signatures de requête v3

L’en-tête X-HubSpot-Signature-v3 sera un hachage HMAC SHA-256 conçu à l’aide du secret de client de votre application associé à des détails de la requête. Il comprendra également un en-tête X-HubSpot-Request-Timestamp. Lors de la validation d’une requête à l’aide de l’en-tête X-HubSpot-Signature-v3, vous devrez :
  • Rejeter la requête si l’horodatage est supérieur à 5 minutes.
  • Dans l’URI de requête, décoder les caractères encodés en URL répertoriés dans le tableau ci-dessous. Vous n’avez pas besoin de décoder le point d’interrogation qui indique le début de la chaîne de requête.
Valeur encodéeValeur décodée
%3A:
%2F/
%3F?
%40@
%21!
%24$
%27'
%28(
%29)
%2A*
%2C,
%3B;
  • Créer une chaîne encodée en UTF-8 qui regroupe les éléments suivants : requestMethod + requestUri + requestBody + horodatage. L’horodatage est fourni par l’en-tête X-HubSpot-Request-Timestamp.
  • Créer un hachage HMAC SHA-256 de la chaîne résultante en utilisant le secret d’application comme secret pour la fonction HMAC SHA-256.
  • Encoder le résultat de la fonction HMAC en Base64.
  • Comparer la valeur de hachage à la signature. Si elle est égale, cette requête a été validée comme provenant de HubSpot. Il est recommandé d’utiliser une comparaison de chaîne à durée fixe pour éviter les attaques temporelles.
Les blocs de code de la section ci-dessous expliquent comment incorporer la validation de requête v3 pour une requête POST si vous exécutiez un service back-end pour gérer les requêtes entrantes. Gardez à l’esprit que les blocs de code ci-dessous omettent certaines dépendances dont vous pourriez avoir besoin pour exécuter un service back-end complet. Confirmez que vous utilisez les dernières bibliothèques stables et sécurisées lors de la mise en œuvre de la validation des requêtes pour votre service spécifique.

Exemples de signatures de requête v3

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

Exemple 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
        }
      }
}