Shopify Webhook HMAC Verification Example

If you're building an application that integrates with Shopify, webhooks are your best friend for real-time updates. Whether it's a new order, a product update, or a customer creation, Shopify sends events directly to your endpoint. But with great power comes great responsibility: how do you know these webhooks are actually from Shopify and haven't been tampered with? This is where HMAC verification comes in.

This article will walk you through the process of verifying Shopify webhooks using HMAC-SHA256, providing concrete code examples in Python and Node.js. We'll also cover common pitfalls and how Hookpeek can simplify your debugging process.

Why Verify Webhooks? The Trust Problem

Imagine your application processes orders. A malicious actor could craft a fake webhook, pretending to be Shopify, and send it to your endpoint. If you don't verify the authenticity of this request, you might:

  • Process fake orders: Leading to financial loss or inventory discrepancies.
  • Update incorrect data: Corrupting your database with bogus information.
  • Trigger unintended actions: Sending emails, creating records, or initiating fulfillment for non-existent events.
  • Be vulnerable to DDoS attacks: An unverified endpoint could be flooded with requests, consuming your resources and potentially bringing down your service.

HMAC (Hash-based Message Authentication Code) verification solves this trust problem. It's a cryptographic mechanism that uses a shared secret key to ensure both the authenticity and integrity of a message.

How Shopify Secures Webhooks: HMAC-SHA256

Shopify uses HMAC-SHA256 to sign its webhooks. This means that for every webhook request it sends to your endpoint, it calculates a unique signature based on:

  1. The raw request body: This is the exact payload Shopify sends.
  2. Your unique shared secret: A secret key known only to Shopify and your application.

This signature is then included in a special HTTP header: X-Shopify-Hmac-SHA256.

When your application receives a webhook, you perform the same calculation using the raw request body and your shared secret. If your computed signature matches the one provided in the X-Shopify-Hmac-SHA256 header, you can be confident that:

  • The request originated from Shopify. Only Shopify (and you) knows the shared secret.
  • The request body has not been tampered with. Any change to the body would result in a different HMAC.

The Verification Process: Step-by-Step

Let's break down the verification process into actionable steps.

Step 1: Get Your Shared Secret

First, you need the shared secret for your webhook. This is generated when you create a webhook in your Shopify admin.

  1. Go to your Shopify admin.
  2. Navigate to Settings > Notifications.
  3. Scroll down to the Webhooks section.
  4. If you're creating a new webhook, the secret will be displayed after creation. If it's an existing one, you might need to regenerate it (which will invalidate the old secret).
  5. Crucially, treat this secret like a password. Do not hardcode it in your application. Use environment variables or a secure secret management service.

Step 2: Capture the Raw Request Body

This is perhaps the most critical step and a common source of errors. You must use the raw, unparsed request body to compute the HMAC.

Many web frameworks (like Express.js with body-parser or Flask with request.get_json()) automatically parse the incoming request body, converting it from a raw string into a JSON object or other data structure. This parsing often involves:

  • Removing whitespace (e.g., newlines, spaces).
  • Reordering JSON keys (though usually not an issue with standard JSON libraries, it's a possibility).
  • Changing character encodings.

Any of these modifications will result in a different HMAC calculation on your end, causing verification to fail. You need the byte-for-byte exact body that Shopify sent.

Step 3: Extract the HMAC Header

The HMAC signature provided by Shopify will be in the X-Shopify-Hmac-SHA256 HTTP header. You'll need to retrieve this value from the incoming request. Note that HTTP headers are generally case-insensitive, but it's good practice to refer to it exactly as Shopify sends it or use a case-insensitive lookup if your framework provides one.

Step 4: Compute Your Own HMAC

Using your shared secret and the raw request body, compute the HMAC-SHA256. The process involves:

  1. Converting the shared secret to bytes.
  2. Converting the raw request body to bytes (typically UTF-8 encoded).
  3. Applying the HMAC-SHA256 algorithm.
  4. Encoding the resulting hash (Shopify uses Base64).

Step 5: Compare the HMACs

Finally, compare your computed HMAC with the one from the X-Shopify-Hmac-SHA256 header.

Important: Always use a constant-time comparison function for this step. Simple string comparison (==) can be vulnerable to timing attacks, where an attacker can deduce information about the secret by measuring the time it takes for a comparison to fail. Most cryptographic libraries provide a secure comparison function (e.g., hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js).

Concrete Example: Python Implementation

Here's a Python example, assuming you're using a web framework like Flask or FastAPI where you can access the raw request body.

import hmac
import hashlib
import base64
import os
from flask import Flask, request, abort

app = Flask(__name__)

# Load your Shopify secret from an environment variable
# NEVER hardcode your secret!
SHOPIFY_WEBHOOK_SECRET = os.environ.get('SHOPIFY_WEBHOOK_SECRET')

if not SHOPIFY_WEBHOOK_SECRET:
    raise ValueError("SHOPIFY_WEBHOOK_SECRET environment variable not set.")

@app.route('/shopify-webhook', methods=['POST'])
def shopify_webhook():
    if not request.data:
        abort(400, description="No request body received.")

    # 1. Get the raw request body
    # request.data in Flask provides the raw bytes of the request body
    raw_body = request.data

    # 2. Get the HMAC header from Shopify
    shopify_hmac = request.headers.get('X-Shopify-Hmac-SHA256')
    if not shopify_hmac:
        abort(401, description="HMAC signature not found in headers.")

    # 3. Compute your own HMAC
    # The secret must be bytes
    secret_bytes = SHOPIFY_WEBHOOK_SECRET.encode('utf-8')

    # The raw_body is already bytes from request.data
    computed_hmac = hmac.new(secret_bytes, raw_body, hashlib.sha256).digest()
    computed_hmac_base64 = base64.b64encode(computed_hmac).decode('utf-8')

    # 4. Compare the HMACs securely
    if hmac.compare_digest(computed_hmac_base64, shopify_hmac):
        print("Webhook verified successfully!")
        # Process the webhook payload here
        # Example: print(request.json) or parse raw_body if needed
        return "Webhook received and verified", 200
    else:
        print("Webhook verification failed!")
        print(f"Shopify HMAC: {shopify_hmac}")
        print(f"Computed HMAC: {computed_hmac_base64}")
        abort(401, description="HMAC verification failed.")

if __name__ == '__main__':
    # Example usage:
    # Set the environment variable: export SHOPIFY_WEBHOOK_SECRET="your_secret_here"
    # Then run: python your_app.py
    app.run(port=5000, debug=True)

To make this runnable, ensure you have Flask installed (pip install Flask). Set the SHOPIFY_WEBHOOK_SECRET environment variable to your actual secret.

Concrete Example: Node.js Implementation

Here's a Node.js example using Express.js. A critical detail here is to use express.raw() middleware to get the raw body before it's parsed.

```javascript const express = require('express'); const crypto = require('crypto'); const app = express(); const port = 3000;

// Load your Shopify secret from an environment variable // NEVER hardcode your secret! const SHOPIFY_WEBHOOK