> ## Documentation Index
> Fetch the complete documentation index at: https://developers.hubspot.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

---
id: e1f1b89b-b8cf-4b5b-906f-68cf366552a0
---

# Validating Requests

> An overview on validating requests originating from HubSpot to your backend service. 

export const BlogPostCTA = ({title, href}) => {
  return <div className="card-condensed-cta">
      <Card title={title} href={href} horizontal={true} cta="Read more on the HubSpot Developer Blog" icon="bullhorn" />
    </div>;
};

To ensure that the requests that your integration is receiving from HubSpot are actually coming from HubSpot, several headers are populated in the request. You can use these headers, along with fields of the incoming request, to verify the signature of the request.

The method used to verify the signature depends on the version of the signature:

* To validate a request using the latest version of the HubSpot signature, use the `X-HubSpot-Signature-V3` header and follow the [associated instructions for validating the v3 version of the signature](#validate-the-v3-request-signature).
* For backwards compatibility, requests from HubSpot also include older versions of the signature. To validate an older version of the signature, check the `X-HubSpot-Signature-Version` header, then follow the associated instructions below based on whether the version is [v2](#validate-requests-using-the-v2-request-signature) or [v1](#validate-requests-using-the-v1-request-signature).

In the instructions below, learn how to derive a hash value from your app's client secret and the fields of an incoming request. Once you compute the hash value, compare it to the signature. If the two are equal, then the request has passed validation. Otherwise, the request may have been tampered with in transit or someone may be spoofing requests to your endpoint.

If you're [building an app](/apps/developer-platform/build-apps/create-an-app) with OAuth authentication, check out the blog post below for guidance on how to validate requests from HubSpot:

<BlogPostCTA title="Blog Post: Production-Ready OAuth Token Management" href="https://developers.hubspot.com/blog/oauth-token-management-hubspot-integrations#security-and-verification" />

## Validate the v3 request signature

The `X-HubSpot-Signature-v3` header will be an HMAC SHA-256 hash built using the client secret of your app combined with details of the request. It will also include a `X-HubSpot-Request-Timestamp` header.

When validating a request using the X-HubSpot-Signature-v3 header, you'll need to

* Reject the request if the timestamp is older than 5 minutes.
* In the request URI, decode any of the URL-encoded characters listed in the table below. You do not need to decode the question mark that denotes the beginning of the query string.

| **Encoded value** | **Decoded value** |
| ----------------- | ----------------- |
| `%3A`             | `:`               |
| `%2F`             | `/`               |
| `%3F`             | `?`               |
| `%40`             | `@`               |
| `%21`             | `!`               |
| `%24`             | `$`               |
| `%27`             | `'`               |
| `%28`             | `(`               |
| `%29`             | `)`               |
| `%2A`             | `*`               |
| `%2C`             | `,`               |
| `%3B`             | `;`               |

* Create a utf-8 encoded string that concatenates together the following: `requestMethod` + `requestUri` + `requestBody` + timestamp. The timestamp is provided by the `X-HubSpot-Request-Timestamp` header.
* Create an HMAC SHA-256 hash of the resulting string using the application secret as the secret for the HMAC SHA-256 function.
* Base64 encode the result of the HMAC function.
* Compare the hash value to the signature. If they're equal then this request has been verified as originating from HubSpot. It's recommended that you use constant-time string comparison to guard against timing attacks.

The code snippets in the section below detail how you could incorporate v3 request validation for a `POST` request if you were running a backend service to handle incoming requests.

<Warning>
  Keep in mind that the code blocks below omit certain dependencies you might need to run a fully-featured backend service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service.
</Warning>

### v3 request signature examples

The code blocks in the tabs below provide examples of validating the v2 request signature using Node.js or Java.

<Tabs>
  <Tab title="Node.js">
    ```js theme={null}
    // 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.
      }
    });
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    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
            }
          }
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $signature = $_SERVER['HTTP_X_HUBSPOT_SIGNATURE_V3'] ?? null;
    $timestamp = $_SERVER['HTTP_X_HUBSPOT_REQUEST_TIMESTAMP'] ?? null;

    if (!$signature || !$timestamp) {
        http_response_code(403);
        exit('Missing signature or timestamp.');
    }

    // Validate timestamp (within 5 minutes)
    $maxSkew = 300;
    if (abs(time() - ((int)($timestamp / 1000))) > $maxSkew) {
        http_response_code(403);
        exit('Expired timestamp.');
    }

    $method = $_SERVER['REQUEST_METHOD'];

    $domain = 'https://' . $_SERVER['HTTP_HOST'];
    $uri = $_SERVER['REQUEST_URI'];

    // Decode URL-encoded characters
    $decodeMap = [
        '%3A' => ':', '%2F' => '/', '%40' => '@',
        '%21' => '!', '%24' => '$', '%27' => "'",
        '%28' => '(', '%29' => ')', '%2A' => '*',
        '%2C' => ',', '%3B' => ';',
    ];
    $uri = strtr(rawurldecode($uri), $decodeMap);

    // Final URI string used in the signature
    $fullUri = $domain . $uri;

    $body = file_get_contents('php://input'); // Raw JSON string
    $clientSecret = 'YOUR_CLIENT_SECRET'; // Replace with the client secret from your app

    $rawString = $method . $fullUri . $body . $timestamp;

    $expectedSignature = base64_encode(
        hash_hmac('sha256', $rawString, $clientSecret, true)
    );

    if (!hash_equals($expectedSignature, $signature)) {
        http_response_code(403);
        exit('Invalid signature.');
    }

    http_response_code(200);
    echo 'Valid webhook';
    ```
  </Tab>
</Tabs>

## Validate requests using the v2 request signature

If your app is handling data from a [webhook action in a workflow](https://knowledge.hubspot.com/workflows/how-do-i-use-webhooks-with-hubspot-workflows), or if you're returning data for an [app card](/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview), the request from HubSpot is sent with the `X-HubSpot-Signature-Version` header set to `v2`. The `X-HubSpot-Signature` header will be an SHA-256 hash built using the client secret of your app combined with details of the request.

To verify this signature, perform the following steps:

* Create a string that concatenates together the following: `Client secret` + `http method` + `URI` + `request body` (if present)
* Create a SHA-256 hash of the resulting string.
* Compare the hash value to the signature.
  * If they're equal then this request has passed validation.
  * If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing requests to your endpoint.

<Tip>
  **Please note:**

  * The URI used to build the source string must exactly match the original request, including the protocol. If you're having trouble validating the signature, ensure that any query parameters are in the exact same order they were listed in the original request.
  * The source string should be UTF-8 encoded before calculating the SHA-256 hash.
</Tip>

### Example for a GET request

For a `GET` request, you'd need your app's client secret and specific fields from the metadata of your request. These fields are listed below with placeholder values included:

* **Client secret:** `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy`
* **HTTP method:** `GET`
* **URI:** `https://www.example.com/webhook_uri`
* **Request body: `""`**

The resulting concatenated string would be: `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri`

After calculating a SHA-256 hash of the concatenated string above, the resulting signature you'd expect to match to the one in the header would be: `eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e`

### Example for a POST request

For a `POST` request, you'd need your app's client secret, specific fields from the metadata of your request, and a string representation of the body of the request (e.g., using `JSON.stringify(request.body)` for a Node.js service). These fields are listed below with placeholder values included:

* **Client secret:** `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy`
* **HTTP method:** `POST`
* **URI:** `https://www.example.com/webhook_uri`
* **Request body:** `{"example_field":"example_value"}`

The resulting concatenated string would be: `yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyPOSThttps://www.example.com/webhook_uri{"example_field":"example_value"}`

After calculating a SHA-256 hash of the concatenated string above, the resulting signature you'd expect to match to the one in the header would be:`9569219f8ba981ffa6f6f16aa0f48637d35d728c7e4d93d0d52efaa512af7900`

After \[SHA-ing] the signature, you could then compare the resulting expected signature to the one provided in the x-hubspot-signature header of the request:

<Warning>
  The code snippets below details how you could incorporate `v2` request validation for a `GET` request if you were running a backend service to handle incoming requests. Keep in mind that the code block below is an example and omits certain dependencies you might need to run a fully-featured backend service. Confirm that you're running the latest stable and secure libraries when implementing request validation for your specific service.
</Warning>

### v2 request signature examples

The code blocks in the tabs below provide examples of validating the v2 request signature using Node.js or Java.

<Tabs>
  <Tab title="Node.js">
    ```js theme={null}
    // 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();
      }
    });
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    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
            }
          }
    }
    ```
  </Tab>
</Tabs>

## Validate requests using the v1 request signature

Some requests from HubSpot's legacy APIs or older webhook formats will be sent with the `X-HubSpot-Signature-Version` header set to `v1`. The `X-HubSpot-Signature` header will be an SHA-256 hash built using the client secret of your app combined with details of the request.

To verify this version of the signature, perform the following steps:

* Create a string that concatenates together the following: `Client secret` + `request body` (if present).
* Create a SHA-256 hash of the resulting string.
* Compare the hash value to the value of the `X-HubSpot-Signature` header:
  * If they're equal then this request has passed validation.
  * If these values do not match, then this request may have been tampered with in-transit or someone may be spoofing requests to your endpoint.

**Example for a request with a body:**

```json theme={null}
// 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 request signature examples

The code blocks in the tabs below provide examples of validating the v2 request signature using Node.js, Java, Python, and Ruby.

<Tabs>
  <Tab title="Node.js">
    ```js theme={null}
    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'
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // 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
        }
    }
    ```
  </Tab>

  <Tab title="Python">
    ```py theme={null}
    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'
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    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"
    ```
  </Tab>
</Tabs>

For each of the examples above, the resulting hash would be: `232db2615f3d666fe21a8ec971ac7b5402d33b9a925784df3ca654d05f4817de`
