# Webhook signature v3 - Can't manage to match header using Python

**URL:** <https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496>\
**Category:** APIs & Integrations\
**Tags:** python, webhooks\
**Created:** [November 27, 2024, 2:30pm UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496 "2024-11-27T14:30:53Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![dataops2tonnes](https://avatars.discourse-cdn.com/v4/letter/d/eb8c5e/32.png) [@dataops2tonnes](https://community.hubspot.com/u/dataops2tonnes)\
**Post date:** [November 27, 2024, 2:30pm UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496/1 "2024-11-27T14:30:53Z")

</div>

Hello !  
I’m trying to follow the [documentation](https://developers.hubspot.com/beta-docs/guides/apps/authentication/validating-requests#validate-the-v3-request-signature) to validate the v3 request signature for webhooks using Python

But i can’t manage to match the v3 signature provided in the request header with the signature that I compute on my side.

For my test, I used a sandbox environment with a private app that I set up. Then i created a webhook subscription using Hubspot UI ( in the webhook section of my private app), and triggered a test request in the details of my webhook subscription.

```lia-code-sample
import hmac
from base64 import b64encode
from hashlib import sha256
import json

# Request data
header_signature = "..."
header_timestamp = "1732715701368"
request_payload = [
 {
 "appId": 4708013,
 "eventId": 100,
 "subscriptionId": 2963051,
 "portalId": 145106154,
 "occurredAt": 1732715551737,
 "subscriptionType": "deal.deletion",
 "attemptNumber": 0,
 "objectId": 123,
 "changeSource": "CRM",
 "changeFlag": "DELETED"
 }
]
# Private app data
webhook_url = "https://my.domain.com/webhook-path"
secret_key = "xxxxxxx-xxxx-xxxx-xxxxxxxxxxx"

msg = "POST" + webhook_url + json.dumps(request_payload) + header_timestamp
print(string)
# POSThttps://my.domain.com/webhook-path[{"appId": 4708013, "eventId": 100, "subscriptionId": 2963051, "portalId": 145106154, "occurredAt": 1732715551737, "subscriptionType": "deal.deletion", "attemptNumber": 0, "objectId": 123, "changeSource": "CRM", "changeFlag": "DELETED"}]1732715701368

computed_signature = b64encode(hmac.new(secret_key.encode("utf-8"), msg=msg.encode("utf-8"), digestmod=sha256).digest()).decode("utf-8")

print(computed_signature == header_signature)
# False

```

But my computed signature doesn’t match with the header signature.

I made sure to use the app’s private key, and the webhook url doesn’t have any URL-encoded characters.

Does anyone have managed to make it work using Python ?

Or can help me solve this issue ?

Big thanks !

---

<div class="post-metadata">

**Author:** ![dataops2tonnes](https://avatars.discourse-cdn.com/v4/letter/d/eb8c5e/32.png) [@dataops2tonnes](https://community.hubspot.com/u/dataops2tonnes)\
**Post date:** [November 27, 2024, 2:46pm UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496/2 "2024-11-27T14:46:59Z")

</div>

Hello !  
I’m following [Hubspot documentation](https://developers.hubspot.com/beta-docs/guides/apps/authentication/validating-requests#validate-the-v3-request-signature) on validating the v3 request signature, but i can’t manage to make it works using Python

Here is my code

```lia-code-sample
import hmac
from base64 import b64encode
from hashlib import sha256
import json

# Request data
header_signature = "P8ucqETtHzOBLWuDCCxNpEvUhXHBwvISTqTfM7DVQCM="
header_timestamp = "1732715701368"
request_payload = [
 {
 "appId": 4708013,
 "eventId": 100,
 "subscriptionId": 2963051,
 "portalId": 145106154,
 "occurredAt": 1732715551737,
 "subscriptionType": "deal.deletion",
 "attemptNumber": 0,
 "objectId": 123,
 "changeSource": "CRM",
 "changeFlag": "DELETED",
 }
]
# Private app data
webhook_url = "https://my.domain.com/webhook-path"
secret_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

string = "POST" + webhook_url + json.dumps(request_payload) + header_timestamp
print(string)
# POSThttps://my.domain.com/webhook-path[{"appId": 4708013, "eventId": 100, "subscriptionId": 2963051, "portalId": 145106154, "occurredAt": 1732715551737, "subscriptionType": "deal.deletion", "attemptNumber": 0, "objectId": 123, "changeSource": "CRM", "changeFlag": "DELETED"}]1732715701368

computed_string = b64encode(
 hmac.new(
 key=secret_key.encode("utf-8"), 
 msg=string.encode("utf-8"), 
 digestmod=sha256
 ).digest()
).decode("utf-8")

print(computed_string == header_signature)
# False

```

For the context, i’m using a sandbox account where I configured a private app.

I then created a webhook subscription in my app using Hubspot UI, and fired a test request in my webhook subscription detail

Does anyone have managed to make it work using Python ?

Or can help me solve my issue ?

Big thanks !

---

<div class="post-metadata">

**Author:** ![dataops2tonnes](https://avatars.discourse-cdn.com/v4/letter/d/eb8c5e/32.png) [@dataops2tonnes](https://community.hubspot.com/u/dataops2tonnes)\
**Post date:** [November 28, 2024, 9:05am UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496/3 "2024-11-28T09:05:21Z")

</div>

Hey Zach

Thanks for your quick reply.

I do use Flask too.  
I looked at your code and I did manage to make it works !

Crazy… Issue was in the jsonification of the request body. It’s good to know that Hubspot doesn’t have whitespaces in the json output when generating the signature, which is not the default behavior of Python’s json.dumps() method

```lia-code-sample
import json

request_payload = [
 {
 "appId": 4708013,
 "eventId": 100,
 "subscriptionId": 2963051,
 "portalId": 145106154,
 "occurredAt": 1732783951647,
 "subscriptionType": "deal.deletion",
 "attemptNumber": 0,
 "objectId": 123,
 "changeSource": "CRM",
 "changeFlag": "DELETED",
 }
]

# Works
print(json.dumps(request_payload, ensure_ascii=False, separators=(",", ":")))
# Doesn't works
print(json.dumps(request_payload))

# Output
# [{"appId":4708013,"eventId":100,"subscriptionId":2963051,"portalId":145106154,"occurredAt":1732783951647,"subscriptionType":"deal.deletion","attemptNumber":0,"objectId":123,"changeSource":"CRM","changeFlag":"DELETED"}]

# [{"appId": 4708013, "eventId": 100, "subscriptionId": 2963051, "portalId": 145106154, "occurredAt": 1732783951647, "subscriptionType": "deal.deletion", "attemptNumber": 0, "objectId": 123, "changeSource": "CRM", "changeFlag": "DELETED"}]

```

Big thanks Zach, it was such a headache haha

I owe you a coffee / a beer / whatever you want

Jimmy Eung

Developer at 2tonnes

Paris, France

---

<div class="post-metadata">

**Author:** ![zach\_threadint](https://d10pqhm3lwygmk.cloudfront.net/user_avatar/community.hubspot.com/zach_threadint/32/46318_2.png) [@zach\_threadint](https://community.hubspot.com/u/zach_threadint)\
**Post date:** [November 28, 2024, 9:38am UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496/4 "2024-11-28T09:38:26Z")

</div>

@dataops2tonnes I’m really glad it helped -- I remember how long it took me to get to the bottom of that issue, so I’m more than happy to help!

---

<div class="post-metadata">

**Author:** ![zach\_threadint](https://d10pqhm3lwygmk.cloudfront.net/user_avatar/community.hubspot.com/zach_threadint/32/46318_2.png) [@zach\_threadint](https://community.hubspot.com/u/zach_threadint)\
**Post date:** [November 28, 2024, 2:21am UTC](https://community.hubspot.com/t/webhook-signature-v3-cant-manage-to-match-header-using-python/121496/5 "2024-11-28T02:21:20Z")

</div>

Hi @dataops2tonnes 👋

I struggled a lot when trying to validate HubSpot requests in Python, so I’m very happy to share with you an example that works for me. Please note, my example uses Python’s Flask app framework. If you’re not using Flask, you’ll have to adjust accordingly (it also relies on 2 variables declared in the given project’s .env file). It supports HubSpot’s validation signature versions 2 and 3.

```lia-code-sample
"""
The following example:
1. Uses Python's Flask app framework
2. Relies on .env file variables, for example:
.env
----
service_domain="https://my.service.domain"
hs_app_client_secret="abc-def"
"""

from datetime import datetime, timezone
import sys
import os
import urllib.parse
import hmac
import base64
import hashlib
import json
from flask import Flask, request

# HubSpot Request Validation Function (Python)
# -- Supports HubSpot Signature Versions 2 and 3
# ==============================================
def hs_request_validation(request_method, request_headers_dict, request_path, request_body):
 """
 Returns True (boolean) for validated requests, False (boolean) for unvalidated requests
 """
 request_headers = {}
 for header in request_headers_dict.keys():
 request_headers[header.lower()] = request_headers_dict[header]
 if "x-hubspot-signature-v3" in request_headers.keys():
 # attempt to validate using V3 request signature
 # reject if request header timestamp is older than 5 minutes
 now_utc = datetime.now(tz=timezone.utc).timestamp()*1000
 request_timestamp = int(request_headers.get("x-hubspot-request-timestamp"))
 if now_utc - request_timestamp > 300000:
 # request timestamp is older than 5 mins, reject
 print(f"** request not validated - timestamp older than 5 mins - {now_utc}, {request_timestamp}")
 sys.stdout.flush()
 return False
 else:
 # continue validation process
 test_string = request_method + os.environ["service_domain"] + urllib.parse.unquote_plus(request_path) + request_body + request_headers.get("x-hubspot-request-timestamp")
 signature = base64.b64encode(
 hmac.new(
 key=bytes(os.environ["hs_app_client_secret"], "utf-8"),
 msg=bytes(test_string, "utf-8"),
 digestmod=hashlib.sha256
 ).digest()
 ).decode()
 if signature == request_headers["x-hubspot-signature-v3"]:
 # request validated - v3
 return True
 else:
 print(f"** request not validated")
 print(f"test_string: {test_string}")
 print(f"test signature: {signature}")
 print(f"hs signature: {request_headers['x-hubspot-signature-v3']}")
 sys.stdout.flush()
 return False
 elif request_headers.get("x-hubspot-signature-version") == "v2":
 # v3 signature not in headers
 # attempt to validate using v2 request signature
 test_string = os.environ["hs_app_client_secret"] + request_method + os.environ["service_domain"] + urllib.parse.unquote_plus(request_path) + request_body
 signature = hashlib.sha256(test_string.encode('utf-8')).hexdigest()
 if signature == request_headers["x-hubspot-signature"]:
 # request validated - v2
 return True
 else:
 print(f"** request not validated")
 print(f"test_string: {test_string}")
 print(f"test signature: {signature}")
 print(f"hs signature: {request_headers['x-hubspot-signature']}")
 sys.stdout.flush()
 return False
 else:
 # invalid request
 print(f"** request not validated - unexpected hs headers")
 print(request_headers)
 sys.stdout.flush()
 return False

# initiate Flask app
app = Flask( __name__ )

# define your service / app endpoint
# this is the endpoint that receives the given request from HubSpot
@app.route("/your-endpoint", methods=['POST'])
def your_endpoint():
 request_validated = hs_request_validation(request.method, dict(request.headers), request.path, json.dumps(request.get_json(), ensure_ascii=False, separators=(",", ":")))
 if request_validated:
 # request validated
 print("request validated")
 sys.stdout.flush()
 else:
 # request not validated
 pass
 print("request not validated")
 sys.stdout.flush()

```

I hope that proves helpful. Please let me know if you have any follow-up questions.
