How to Secure and Verify HubSpot Webhooks with Hookdeck
HubSpot is one of the most widely adopted CRM and marketing platforms, used by over 200,000 businesses to manage contacts, deals, tickets, and marketing automation. HubSpot's webhooks API delivers real-time notifications when CRM data changes, enabling integrations with external databases, automation platforms, and notification systems.
However, accepting webhooks without proper verification leaves your application vulnerable to spoofed requests from malicious actors. In this guide, we'll show you how to secure and verify HubSpot webhooks manually and with Hookdeck, ensuring that every webhook your application processes genuinely originated from HubSpot. For a comprehensive overview of HubSpot webhook capabilities, see our guide to HubSpot webhooks.
Why webhook security matters
Webhooks are HTTP callbacks that notify your application when events occur. Without verification, an attacker could send fake webhook payloads to your endpoint, potentially triggering unauthorized actions like:
- Creating or modifying CRM records based on fabricated events
- Triggering automated workflows with spoofed data
- Corrupting your customer database with fake contact or deal changes
- Gaining unauthorized access to systems that trust webhook-triggered updates
HubSpot addresses this with multiple signature versions, with v3 (HMAC-SHA256) being the recommended approach. By verifying signatures, you can be confident that webhooks are authentic and haven't been tampered with in transit.
How HubSpot webhook signatures work
HubSpot supports three signature versions, each with increasing security:
| Version | Algorithm | Replay protection | Recommended |
|---|---|---|---|
| v1 | SHA-256 (no HMAC key) | No | No |
| v2 | SHA-256 (no HMAC key) | No | No |
| v3 | HMAC-SHA256 | Yes (timestamp) | Yes |
Signature v3 (recommended)
v3 is the most secure option and the only version that uses HMAC and includes replay attack protection.
Every v3 webhook request includes two headers:
X-HubSpot-Signature-v3— Base64-encoded HMAC-SHA256 signatureX-HubSpot-Request-Timestamp— Millisecond-precision Unix timestamp
The signature is computed by:
- Concatenating:
{request_method}{request_uri}{request_body}{timestamp}- For example:
POSThttps://your-endpoint.com/webhooks/hubspot[{"objectId":123,...}]1671552777
- For example:
- Computing HMAC-SHA256 using the app's client secret as the key
- Base64-encoding the result
Signature v1 and v2 (legacy)
- v1:
SHA-256(client_secret + request_body)— simple concatenation, no HMAC key, no replay protection - v2:
SHA-256(client_secret + http_method + URI + request_body)— adds method and URI but still no HMAC key
v1 and v2 do not use HMAC — they simply prepend the secret to the message before hashing. This makes them weaker than v3 and they should be considered legacy.
Getting your HubSpot client secret
The signing key for HubSpot webhooks is your app's client secret (not a separate webhook-specific secret):
For public apps
- Go to your HubSpot Developer Account
- Select your app
- Navigate to Auth
- Find the Client secret field
For private apps
- In your HubSpot account, go to Settings > Integrations > Private Apps
- Select your private app
- Find the Client secret in the app settings
Verifying webhook signatures manually (v3)
Verification process overview
- Extract the
X-HubSpot-Request-Timestampheader and reject requests older than 5 minutes (prevents replay attacks) - Concatenate:
POST+ full request URI + raw request body + timestamp - Compute HMAC-SHA256 using the client secret as the key
- Base64-encode the result
- Compare with the
X-HubSpot-Signature-v3header using a timing-safe comparison
Node.js verification example
const express = require("express");
const crypto = require("crypto");
const app = express();
const HUBSPOT_CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET;
function verifyHubSpotWebhook(rawBody, req) {
const timestamp = req.headers["x-hubspot-request-timestamp"];
const signature = req.headers["x-hubspot-signature-v3"];
if (!timestamp || !signature) {
console.error("Missing HubSpot signature headers");
return false;
}
// Reject requests older than 5 minutes (timestamp is in milliseconds)
const currentTime = Date.now();
if (Math.abs(currentTime - parseInt(timestamp, 10)) > 300000) {
console.error("Webhook timestamp is outside tolerance window");
return false;
}
// Build the signed string
// Use the full request URL as HubSpot sees it
const requestUri = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
const signedString = `POST${requestUri}${rawBody}${timestamp}`;
// Compute HMAC-SHA256
const computedSignature = crypto
.createHmac("sha256", HUBSPOT_CLIENT_SECRET)
.update(signedString, "utf8")
.digest("base64");
// Use timing-safe comparison
const receivedBuffer = Buffer.from(signature, "utf8");
const expectedBuffer = Buffer.from(computedSignature, "utf8");
if (receivedBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}
app.post(
"/webhooks/hubspot",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString();
if (!verifyHubSpotWebhook(rawBody, req)) {
return res.status(401).json({ error: "Invalid signature" });
}
const events = JSON.parse(rawBody);
// HubSpot sends events in batches (up to 100 per request)
for (const event of events) {
switch (event.subscriptionType) {
case "contact.creation":
handleContactCreated(event);
break;
case "deal.propertyChange":
handleDealPropertyChange(event);
break;
default:
console.log(`Received event: ${event.subscriptionType}`);
}
}
res.status(200).json({ received: true });
}
);
app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});
Python verification example
import hmac
import hashlib
import base64
import time
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
HUBSPOT_CLIENT_SECRET = os.environ.get("HUBSPOT_CLIENT_SECRET")
def verify_hubspot_webhook(raw_body, req):
"""Verify a HubSpot webhook v3 signature."""
timestamp = req.headers.get("X-HubSpot-Request-Timestamp", "")
signature = req.headers.get("X-HubSpot-Signature-v3", "")
if not timestamp or not signature:
return False
# Reject requests older than 5 minutes (timestamp in milliseconds)
current_time = int(time.time() * 1000)
if abs(current_time - int(timestamp)) > 300000:
return False
# Build the signed string
request_uri = request.url
signed_string = f"POST{request_uri}{raw_body}{timestamp}"
# Compute HMAC-SHA256
computed_signature = base64.b64encode(
hmac.new(
HUBSPOT_CLIENT_SECRET.encode("utf-8"),
signed_string.encode("utf-8"),
hashlib.sha256
).digest()
).decode("utf-8")
# Use timing-safe comparison
return hmac.compare_digest(computed_signature, signature)
@app.route("/webhooks/hubspot", methods=["POST"])
def handle_webhook():
raw_body = request.get_data(as_text=True)
if not verify_hubspot_webhook(raw_body, request):
abort(401, "Invalid signature")
events = request.get_json()
for event in events:
print(f"Received verified {event.get('subscriptionType')} event")
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)
Critical security best practices
Use v3 signatures
Always verify using v3 (X-HubSpot-Signature-v3). v1 and v2 lack HMAC keying and replay protection, making them significantly weaker.
Use timing-safe comparisons
Never use standard equality operators (== or ===) to compare signatures. These can leak timing information that attackers exploit. Always use:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - Go:
subtle.ConstantTimeCompare()
Implement replay attack prevention
Check the X-HubSpot-Request-Timestamp header and reject webhooks where the timestamp differs from your server time by more than 5 minutes. Note that HubSpot timestamps are in milliseconds, not seconds.
Match the full request URI
The v3 signature includes the full request URI as HubSpot sees it. If your application sits behind a reverse proxy, load balancer, or CDN that modifies the URL, the URI you reconstruct may not match what HubSpot used to compute the signature. Ensure your application has access to the original request URL.
Handle batched events
HubSpot sends up to 100 events per request. Each event in the batch may relate to a different CRM object or subscription type. Process each event individually and idempotently.
Preserve the raw request body
The HMAC signature is computed on the exact bytes HubSpot sends. Any parsing, reformatting, or middleware that modifies the body before verification will produce a different signature.
Simplifying verification with Hookdeck
Manually implementing and maintaining webhook verification across multiple providers can be complex and error-prone. Hookdeck provides a webhook gateway that handles verification automatically, allowing you to focus on processing events.
What is Hookdeck?
Hookdeck provides an event gateway that sits between webhook providers (like HubSpot) and your application. It provides:
- Automatic signature verification
- Event queuing and retry logic
- Request logging and debugging tools
- Local development tunneling
Setting up HubSpot webhooks with Hookdeck
Step 1: Install the Hookdeck CLI
npm install hookdeck-cli -g
yarn global add hookdeck-cli
brew install hookdeck/hookdeck/hookdeck
-
scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git -
scoop install hookdeck
-
Download the latest release's tar.gz file.
-
tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz -
./hookdeck
Step 2: Authenticate
hookdeck login
This opens your browser for authentication. If you don't have a Hookdeck account, you can create one during this step.
Step 3: Create a connection
hookdeck listen 3000 hubspot-source --path /webhooks/hubspot
This command:
- Creates a public URL for receiving webhooks
- Forwards events to
http://localhost:3000/webhooks/hubspot - Displays the Source URL to configure in HubSpot
Step 4: Configure HubSpot
- Copy the Hookdeck Source URL from the CLI output
- In your HubSpot developer account, select your app
- Navigate to Webhooks
- Set the Target URL to your Hookdeck Source URL
- Create subscriptions for the events you want to receive
- Save the configuration
Step 5: Configure source verification
- Open the Hookdeck Dashboard
- Navigate to Connections and select your source
- Under Advanced Source Configuration, enable Source Authentication
- Select HubSpot from the list of platforms (if available), or configure using HMAC with:
- Algorithm: SHA-256
- Header: X-HubSpot-Signature-v3
- Encoding: Base64
- Secret: Your HubSpot app client secret
- Click Save
How Hookdeck verification works
When verification is enabled:
- Hookdeck receives the webhook from HubSpot
- Hookdeck validates the
X-HubSpot-Signature-v3against the payload using your client secret - Valid requests are forwarded to your endpoint with
x-hookdeck-verified: true - Invalid requests are rejected and logged as "Verification Failed"
This means your application can trust any request from Hookdeck without implementing its own HubSpot signature verification logic. You only need to implement Hookdeck's signature verification on your server.
Troubleshooting common issues
Signature mismatch
If signatures don't match, verify:
- Raw body usage: Ensure you're using the exact bytes received, not a parsed/stringified version
- Secret accuracy: Confirm you're using the app's client secret, not the API key or a different credential
- Request URI: The URI must match exactly what HubSpot used — check for proxy rewrites, port differences, or scheme changes (HTTP vs HTTPS)
- Timestamp format: HubSpot timestamps are in milliseconds (not seconds) — make sure your tolerance check accounts for this
- Encoding: v3 signatures are Base64-encoded, not hex
Missing signature headers
- Ensure your app has webhook subscriptions configured in the HubSpot developer portal
- Private app webhooks use the same signature mechanism but are configured differently — check your private app settings
Event ordering issues
HubSpot does not guarantee event ordering. Use the occurredAt timestamp within each event to determine the actual sequence. For property change events, compare propertyValue against the current CRM state rather than assuming order.
Conclusion
Securing HubSpot webhooks requires verifying the HMAC-SHA256 signature (v3) on every incoming request. While you can implement this verification manually, Hookdeck simplifies the process by providing automatic signature verification, event management, and development tools, allowing you to focus on building your integration rather than infrastructure.