Hello,
I hope you can help me. I’m having trouble debugging my code, specifically with matching the HMAC that Shopify sends me with the one I calculate using the base64
module. I’m using flask for my server.
I’ve made sure that I’m using the correct SHOPIFY_API_SECRET
. When I use FreeFormatter’s HMAC Generator, I get the same output as the one generated by my debug logs (DEBUG:shopify:HMAC method 2 (hexdigest): 775d8fbd320c53aaa4cbc18dee98edbbc2b69a33e3743a05d3a4bb63ad7faa9f
).
However, when I calculate the HMAC in my code using the base64
module, the result doesn’t match Shopify’s HMAC. It seems straightforward, but I can’t figure out why the values are different. Do you have any suggestions on what I might be missing? You will find my code and some screenshot below.
Thank you in advance for your help!
# HMAC debugging function
def debug_hmac(secret, data, received_hmac):
# Method 1 - Standard UTF-8
digest1 = hmac.new(secret.encode('utf-8'), data, hashlib.sha256).digest()
hmac1 = base64.b64encode(digest1).decode('utf-8').strip() # Utilisation de base64 au lieu de codecs
# Method 2 - Hexadecimal
hmac2 = hmac.new(secret.encode('utf-8'), data, hashlib.sha256).hexdigest()
# Method 3 - ASCII
digest3 = hmac.new(secret.encode('ascii'), data, hashlib.sha256).digest()
hmac3 = base64.b64encode(digest3).decode('utf-8').strip() # Utilisation de base64 au lieu de codecs
logger.debug(f"Received HMAC: {received_hmac}")
logger.debug(f"HMAC method 1 (UTF-8 + base64): {hmac1}")
logger.debug(f"HMAC method 2 (hexdigest): {hmac2}")
logger.debug(f"HMAC method 3 (ASCII + base64): {hmac3}")
# Check if any of the methods match
match1 = hmac.compare_digest(received_hmac, hmac1)
match2 = received_hmac == hmac2
match3 = hmac.compare_digest(received_hmac, hmac3)
logger.debug(f"Match method 1: {match1}")
logger.debug(f"Match method 2: {match2}")
logger.debug(f"Match method 3: {match3}")
return match1 or match2 or match3
def verify_webhook(data, hmac_header):
# Calculate HMAC with Shopify API key and webhook data
digest = hmac.new(SHOPIFY_API_SECRET.encode('utf-8'), data, hashlib.sha256).digest()
# Encode in base64 using base64
computed_hmac = base64.b64encode(digest).decode('utf-8').strip() # Utilisation de base64 au lieu de codecs
# Compare the calculated HMAC with the one in the request header
is_valid = hmac.compare_digest(computed_hmac, hmac_header)
# Return validity and the computed HMAC
return is_valid, computed_hmac
# 🔹 Webhook for 'orders/create' and 'orders/paid' events
@app.route('/webhook/order/create', methods=['POST'])
def order_create_webhook():
logger.debug("Webhook received")
shop = request.headers.get('X-Shopify-Shop-Domain')
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
topic = request.headers.get('X-Shopify-Topic')
data = request.get_data() # Raw request data
logger.debug(f"HMAC Header: {hmac_header}")
logger.debug(f"Topic: {topic}")
logger.debug(f"Data length: {len(data)}")
# Display the full data as a string
logger.debug(f"Complete data: {data.decode('utf-8')}")
# Verify the webhook and retrieve the computed HMAC
verified, computed_hmac = verify_webhook(data, hmac_header)
if verified:
print("HMAC is valid!")
else:
print("HMAC is invalid!")
# Display the computed HMAC
print(f"computed_hmac={computed_hmac}")
# Use the HMAC debugging function to check all methods
hmac_valid = debug_hmac(SHOPIFY_API_SECRET, data, hmac_header)
if not hmac_valid:
logger.error("HMAC invalid after checking all methods")
return "Invalid HMAC.", 403
logger.debug("HMAC validated successfully!")
This code is for Django but could you try something like and see if it works? The type of request.body
is bytes
.
import base64
import hashlib
import hmac
from django.conf import settings
from django.http import HttpRequest
def is_webhook_valid(request: HttpRequest) -> bool:
"""Validates webhook message for an app"""
body_hash = hmac.new(
bytes(settings.SHOPIFY_API_SECRET, "utf-8"), request.body, hashlib.sha256
)
b64_hash = base64.b64encode(body_hash.digest())
return b64_hash == bytes(
request.META.get("HTTP_X_SHOPIFY_HMAC_SHA256", "none"), "utf-8"
)
Hi @Daniel_Ablestar ,
Thank you for your help. It still doesn’t work.
Here is the new code I have used :
def is_webhook_valid(data: bytes, hmac_header: str) -> bool:
"""Validates the webhook message for the Shopify app"""
# Calculate the HMAC SHA256 hash on the raw request data
body_hash = hmac.new(
bytes(SHOPIFY_API_SECRET, "utf-8"), data, hashlib.sha256
)
# Encode the digest in base64
b64_hash = base64.b64encode(body_hash.digest()).decode('utf-8') # Decode to UTF-8 for comparison
# Compare the calculated signature with the header signature
return b64_hash, hmac.compare_digest(b64_hash, hmac_header) # Return both the b64_hash and the result of the comparison
@app.route('/webhook/order/create', methods=['POST'])
def order_create_webhook():
logger.debug(" Webhook received")
# Extract headers and request data
shop = request.headers.get('X-Shopify-Shop-Domain')
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256', "")
topic = request.headers.get('X-Shopify-Topic')
data = request.get_data() # Raw data from the request
logger.debug(f" HMAC Header: {hmac_header}")
logger.debug(f" Webhook Topic: {topic}")
logger.debug(f" Data Length: {len(data)}")
logger.debug(f" First 300 characters of data: {data[:300]}")
# Shopify webhook verification
b64_hash, verified = is_webhook_valid(data, hmac_header) # Get both the b64_hash and verification result
if not verified:
logger.error(f" Invalid HMAC! (Computed: {b64_hash})") # Now using the computed b64_hash
return jsonify({"status": "error", "message": "Invalid HMAC"}), 403
logger.debug(" HMAC successfully validated!")
And here is the outcome :
DEBUG:shopify: Webhook received
DEBUG:shopify: HMAC Header: KXD/Ds/qK9uqm2+uGe9/Z3J+qYUw4FR318bzYb3fKSk=
DEBUG:shopify: Webhook Topic: orders/paid
DEBUG:shopify: Data Length: 7936
DEBUG:shopify: First 300 characters of data: b’{“id”:820982911946154508,“admin_graphql_api_id”:“gid:\/\/shopify\/Order\/820982911946154508”,“app_id”:null,“browser_ip”:null,“buyer_accepts_marketing”:true,“cancel_reason”:“customer”,“cancelled_at”:“2025-03-19T16:15:26-04:00”,“cart_token”:null,“checkout_id”:null,“checkout_token”:null,"client_details’
ERROR:shopify: Invalid HMAC! (Computed: BfxUunDHAn9OI0UPt/rPnTz7i5Ivt20wjJ/CKzhHRmo=)
INFO:werkzeug:127.0.0.1 - - [19/Mar/2025 20:15:27] “POST /webhook/order/create HTTP/1.0” 403 -
it’s such a headache 
Hi @Merouane, the code snippets provided look like they should work. I created the following small python app with Flask, added a webhook subscription for orders/create
and products/update
to my shop and was able to receive webhooks and validate the HMAC. In the snippet below you will need to update the SHOPIFY_API_SECRET
with your client secret:
from flask import Flask, request
import hmac
import hashlib
import base64
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
SHOPIFY_API_SECRET = "YOUR_CLIENT_SECRET_KEY"
app = Flask(__name__)
@app.route('/', methods=['POST'])
def receive_post():
headers = request.headers
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256', "")
body = request.data.decode('utf-8')
data = request.get_data()
body_hash = hmac.new(
bytes(SHOPIFY_API_SECRET, "utf-8"), data, hashlib.sha256
)
b64_hash = base64.b64encode(body_hash.digest()).decode('utf-8')
logger.debug("Header HMAC: " + hmac_header + " calculated: " + b64_hash)
hmac_match = b64_hash == hmac_header
if not hmac_match:
logger.debug("Invalid HMAC!")
return 'Invalid signature', 401
logger.debug("HMAC successfully validated!")
return 'Received', 200
if __name__ == '__main__':
app.run(debug=True)
Hello Dave,
Thank you for your help.
Unforunately your code is not working for me … No idea why.
I shared you my screen. Maybe I do something wrong ?
Here is the link of my video : https://youtu.be/5XrSG7_pPvY
@Merouane can you confirm which client secret you are using? If you are using the ones for your app from your original screenshot this may be the issue as each app has it’s own secret key.
When you add a webhook subscription through Admin Settings → Notifications → Webhooks these are not being associated to your app and use their own secret key. On the bottom of that page you will see Your webhooks will be signed with API_CLIENT_SECRET_KEY
. If you use this key when you select the Send test
option, I would expect the HMAC to validate. Can you confirm?
Hi Dave,
Thank you!
Indeed, when I copy and paste the one below my webhook, it works. If I understand correctly, this means that when I use the api_client_secret
from my app, it only works if the webhook has been previously set up by my app. Is that correct?
@Merouane the webhook subscriptions that show under the Admin Settings → Notifications → Webhooks page are distinct from subscriptions that are created with your app. You will not see webhook subscriptions on this page that were created from your app.
If you were to add a subscription using the TOML file, or the GraphQL webhook subscription create mutation you would need to use your app’s api_client_secret
to validate the HMAC for webhooks that are received from those subscriptions.
1 Like