Hookbase
LoginGet Started Free
Back to Blog
Tutorial

Shopify Webhook Signature Verification, Explained

Shopify HMAC verification trips up almost every first-time integrator. Here is exactly how the signature is computed, what goes wrong, and a working implementation in Node, Python, Go, and Ruby.

Hookbase Team
April 25, 2026
6 min read

Why This Always Breaks the First Time

You wire up a Shopify webhook, point it at your endpoint, and your signature check returns false on every event. You triple-check the secret. You log the header. The math looks right. Nothing works.

The problem is almost never the secret. It's that you're hashing the wrong bytes.

How Shopify Signs Webhooks

Shopify sends an X-Shopify-Hmac-Sha256 header containing a base64-encoded HMAC-SHA256 of the raw request body, computed with your shared secret as the key.

Three things matter, in this exact order:

  1. Raw bytes, not parsed JSON. The HMAC is over the exact bytes Shopify sent. The instant your framework parses the body and re-serializes it, key order shifts and whitespace changes — your hash no longer matches.
  2. Base64, not hex. Shopify uses base64. GitHub uses hex. Stripe uses hex. Mixing these up is a common copy-paste mistake.
  3. The secret is the one from the webhook config, not your API secret key. They're different.

The Working Implementation

Node.js (Express)

import express from 'express';
import crypto from 'crypto';

const app = express();

app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-Shopify-Hmac-Sha256');
    const computed = crypto
      .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
      .update(req.body)
      .digest('base64');

    if (
      !signature ||
      !crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(computed)
      )
    ) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());
    console.log('Topic:', req.header('X-Shopify-Topic'));
    res.status(200).send('ok');
  }
);

The critical line is express.raw({ type: 'application/json' }). The default express.json() middleware parses the body into an object — by the time you'd hash it, the original bytes are gone.

Python (Flask)

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

app = Flask(__name__)
SECRET = os.environ['SHOPIFY_WEBHOOK_SECRET'].encode()

@app.post('/webhook')
def webhook():
    signature = request.headers.get('X-Shopify-Hmac-Sha256', '')
    computed = base64.b64encode(
        hmac.new(SECRET, request.get_data(), hashlib.sha256).digest()
    ).decode()
    if not hmac.compare_digest(signature, computed):
        abort(401)
    return 'ok'

request.get_data() returns raw bytes. Do not use request.json.

Go

func verify(body []byte, signature, secret string) bool {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write(body)
    expected := base64.StdEncoding.EncodeToString(h.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

Read the body with io.ReadAll(r.Body) once, then use those bytes for both verification and parsing.

Ruby (Rails)

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def receive
    body = request.body.read
    signature = request.headers['X-Shopify-Hmac-Sha256']
    computed = Base64.strict_encode64(
      OpenSSL::HMAC.digest('sha256', ENV['SHOPIFY_WEBHOOK_SECRET'], body)
    )
    head :unauthorized and return unless ActiveSupport::SecurityUtils.secure_compare(signature, computed)
    head :ok
  end
end

The Five Mistakes to Avoid

1. Letting your framework parse the body first. This is the #1 cause of "my HMAC doesn't match." Read raw bytes, hash those, then parse.

2. Using == instead of constant-time comparison. Timing attacks against HMAC verification are real. Always use the language's secure-compare primitive.

3. Re-encoding the body to UTF-8 unnecessarily. Shopify sends bytes. Treat them as bytes. Encoding round-trips can change them.

4. Hashing in hex. A two-character mistake — .digest('hex') instead of .digest('base64') — that produces a wrong hash that's exactly the right length, so the comparison fails silently.

5. Using the wrong secret. The webhook secret is shown once when you create the webhook, either in the Shopify Partner dashboard or via the Admin API. Store it then; you can't retrieve it later.

When Verification Should Happen

Verify the signature before doing anything else with the payload — including logging it. An attacker who can post arbitrary payloads to your endpoint can poison your logs, trigger downstream alerts, or fill your queue with junk. The signature check is the first gate.

// 1. Read raw body
// 2. Verify signature -> return 401 if bad
// 3. Parse JSON
// 4. Acknowledge with 200
// 5. Process async

How Hookbase Removes the Footgun

Hookbase verifies Shopify HMACs (and 30+ other providers) before the event reaches your handler. You configure the secret once, and every event that arrives at your endpoint is guaranteed verified. You can't accidentally skip it, parse the body too early, or compare in non-constant time — because there's no signature code in your handler at all.

Try it free.

shopifywebhookssignature-verificationsecuritytutorial

Related Articles

Reference

Webhook Retries: What Every Provider Does Differently

Stripe retries for 3 days. GitHub gives up after one failure. Shopify retries 19 times. Knowing the rules for each provider is the difference between losing events and not. A reference table plus what it means for your handler.

Best Practices

Idempotency Keys for Webhooks: A Practical Guide

Webhooks get retried. Without idempotency, that means duplicate orders, double charges, and angry customers. Here is how to design a deduplication strategy that actually works.

Security

10 Webhook Providers That Don't Sign Their Payloads (And What to Do)

Not every provider signs webhooks. When the provider hands you an unauthenticated POST, you have to invent your own security model. Here are the providers, the reasons, and three patterns that work.

Ready to Try Hookbase?

Start receiving, transforming, and routing webhooks in minutes.

Get Started Free
Hookbase

Reliable webhook infrastructure for modern teams. Built on Cloudflare's global edge network.

Product

  • Features
  • Pricing
  • Use Cases
  • Integrations
  • ngrok Alternative

Resources

  • Documentation
  • API Reference
  • CLI Guide
  • Blog
  • FAQ

Free Tools

  • All Tools
  • Webhook Bin
  • HMAC Calculator
  • JSONata Playground
  • Cron Builder
  • Payload Formatter
  • Local Testing

Legal

  • Privacy Policy
  • Terms of Service
  • Contact
  • Status

© 2026 Hookbase. All rights reserved.