Última modificación: 22 de agosto de 2025
Para asegurarte de que las solicitudes que recibe tu integración realmente provienen de HubSpot, la solicitud incluye varios encabezados. Puedes usar estos encabezados, junto con los campos de la solicitud entrante, para verificar la firma de la solicitud. El método utilizado para verificar la firma depende de la versión de la firma:
  • Para validar una solicitud usando la última versión de la firma de HubSpot, usa el encabezado X-HubSpot-Signature-V3 y sigue las instrucciones asociadas para validar la versión v3 de la firma.
  • Para la compatibilidad con versiones anteriores, las solicitudes de HubSpot también incluyen versiones anteriores de la firma. Para validar una versión anterior de la firma, verifica el encabezado X-HubSpot-Signature-Version, y luego sigue las instrucciones asociadas que aparecen abajo según si la versión es v1 o v2.
En las siguientes instrucciones, encuentra más información sobre cómo obtener un valor del hash del secreto del cliente de tu aplicación y los campos de una solicitud entrante. Una vez que calcules el valor del hash, compáralo con la firma. Si los dos son iguales, entonces la solicitud pasó la validación. De lo contrario, la solicitud puede haber sido manipulada en el tránsito o alguien puede estar falsificando solicitudes en tu endpoint.

Validar solicitudes usando la firma de solicitud v1

Si tu aplicación está suscrita a eventos de objetos del CRM a través de la API de webhooks, las solicitudes de HubSpot se enviarán con el encabezado X-HubSpot-Signature-Version definido como v1. El encabezado X-HubSpot-Signature será un hash SHA-256 creado usando el secreto del cliente de tu aplicación junto con detalles de la solicitud. Para verificar esta versión de la firma, realiza los siguientes pasos:
  • Crea una cadena que concatene lo siguiente: Client secret + request body (si está presente).
  • Crea un hash SHA-256 de la cadena resultante.
  • Compara el valor del hash con el valor del encabezado X-HubSpot-Signature:
    • Si son iguales, esta solicitud ha superado la validación.
    • Si estos valores no coinciden, es posible que esta solicitud haya sido manipulada en tránsito o que alguien esté falsificando solicitudes a tu endpoint.
Ejemplo para una solicitud con un cuerpo:
//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}
]

Ejemplos de firma de solicitud v1:

Ejemplo en 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'

Ejemplo de 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"

Ejemplo de 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'

Ejemplo de 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
    }
}
El hash resultante sería el siguiente: 232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de

Validar solicitudes usando la firma de solicitud v2

Si tu aplicación está manejando datos de una acción de webhook en un workflow o si estás devolviendo datos para una tarjeta del CRM personalizada, la solicitud de HubSpot se envía con el encabezado X-HubSpot-Signature-Version definido como v2. El encabezado X-HubSpot-Signature será un hash SHA-256 creado usando el secreto del cliente de tu aplicación junto con detalles de la solicitud. Para verificar esta firma, realiza los siguientes pasos:
  • Crea una cadena que concatene lo siguiente: Client secret + http method + URI + request body (si está presente).
  • Crea un hash SHA-256 de la cadena resultante.
  • Compara el valor del hash a la firma.
    • Si son iguales, esta solicitud ha superado la validación.
    • Si estos valores no coinciden, es posible que esta solicitud haya sido manipulada en tránsito o que alguien esté falsificando solicitudes a tu endpoint.
Notas:
  • La URI utilizada para crear la cadena de origen debe coincidir exactamente con la solicitud original, incluyendo el protocolo. Si tienes problemas para validar la firma, asegúrate de que cualquier parámetro de consulta esté en el mismo orden que se enumeró en la solicitud original.
  • La cadena de origen debe tener codificación UTF-8 antes de calcular el hash SHA-256.

Ejemplo de una solicitud GET:

En el caso de una solicitud GET, necesitarías el secreto del cliente de tu aplicación y campos específicos de los metadatos de la solicitud. Estos campos se enumeran a continuación con valores de marcadores de posición incluidos:
  • Secreto del cliente yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • Método HTTP: GET
  • URI https://www.example.com/webhook_uri
  • Cuerpo de la solicitud: ""
La cadena concatenada resultante sería: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri Después de calcular un hash SHA-256 de la cadena concatenada anterior, la firma resultante que esperarías que coincidiera con la del encabezado sería: eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e

Ejemplo de una solicitud POST

En el caso de una solicitud POST, necesitarías el secreto del cliente de tu aplicación, campos específicos de los metadatos de la solicitud y una representación de cadena del cuerpo de la solicitud (por ejemplo, usar JSON.stringify(request.body) para un servicio de Node.js). Estos campos se enumeran a continuación con valores de marcadores de posición incluidos:
  • Secreto del cliente yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
  • Método HTTP: POST
  • URI https://www.example.com/webhook_uri
  • Cuerpo de la solicitud: {"example_field":"example_value"}
La cadena concatenada resultante sería: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyPOSThttps://www.example.com/webhook_uri{"example_field":"example_value"} Después de calcular un hash SHA-256 de la cadena concatenada anterior, la firma resultante que esperarías que coincidiera con la del encabezado sería:9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900 Después de la firma [SHA-ing], podrías comparar la firma esperada resultante con la proporcionada en el encabezado x-hubspot-signature de la solicitud: El fragmento de código de Node.js que aparece abajo detalla cómo podrías incorporar la validación de solicitudes v2 correspondiente a una solicitud GET si estuvieras ejecutando un servidor Express para manejar las solicitudes entrantes. Ten en cuenta que el bloque de código que aparece abajo es un ejemplo y omite ciertas dependencias que podrías necesitar para ejecutar un servicio Express con todas las funciones. Confirma que estás ejecutando las bibliotecas estables y seguras más recientes al implementar la validación de solicitudes para tu servicio específico.

Ejemplos de firma de solicitud v2:

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

Ejemplo de 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
        }
      }
}

Validar la firma de solicitud v3

El encabezado X-HubSpot-Signature-v3 será un hash SHA-256 creado usando el secreto del cliente de tu aplicación junto con detalles de la solicitud. También incluirá un encabezado X-HubSpot-Request-Timestamp. Al validar una solicitud que utiliza el encabezado X-HubSpot-Signature-v3, deberás
  • Rechazar la solicitud si la marca de tiempo es anterior a 5 minutos.
  • En la URI de la solicitud, decodifica cualquiera de los caracteres codificados por URL que se enumeran en la siguiente tabla. No necesitas decodificar el signo de interrogación que denota el comienzo de la cadena de consulta.
Valor codificadoValor decodificado
%3A:
%2F/
%3F?
%40@
%21!
%24$
%27'
%28(
%29)
%2A*
%2C,
%3B;
  • Crea una cadena codificada utf-8 que concatene lo siguiente: requestMethod + requestUri + requestBody + marca de tiempo. La marca de tiempo es proporcionada por el encabezado X-HubSpot-Request-Timestamp.
  • Crea un hash SHA-256 de la cadena resultante usando el secreto de la aplicación como el secreto de la función HMAC SHA-256.
  • Base64 codifica el resultado de la función HMAC.
  • Compara el valor del hash a la firma. Si son iguales, significa que esta solicitud se verificó como proveniente de HubSpot. Se recomienda que uses la comparación de cadenas en tiempo constante para protegerte de los ataques de sincronización.
Los fragmentos de código de la siguiente sección detallan cómo podrías incorporar la validación de solicitudes v3 para una solicitud POST si estuvieras ejecutando un servicio backend para gestionar las solicitudes entrantes. Ten en cuenta que el bloque de código que aparece abajo es un ejemplo y omite ciertas dependencias que podrías necesitar para ejecutar un servicio Express con todas las funciones. Confirma que estás ejecutando las bibliotecas estables y seguras más recientes al implementar la validación de solicitudes para tu servicio específico.

Ejemplos de firma de solicitud v3:

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

Ejemplo de 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
        }
      }
}