Verify Shopify Webhook Signature (X-Shopify-Hmac-SHA256 Header)

Shopify signs webhook requests using the X-Shopify-Hmac-SHA256 header. To verify the signature, you must compute an HMAC-SHA256 hash of the raw request body using your webhook secret and compare it to the Base64-encoded header value.

🔒 Your webhook secret never leaves your browser. All verification happens client-side.

Find this in your Shopify Admin → Settings → Notifications → Webhooks

Code Examples

Copy-paste examples for verifying Shopify webhook signatures in your application.

Node.js / Express

Using crypto module
const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANT: Use raw body for webhook routes
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const hmacHeader = req.headers['x-shopify-hmac-sha256'];
    const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
    
    if (!hmacHeader) {
      return res.status(400).send('Missing HMAC header');
    }
    
    // Compute expected HMAC (Base64-encoded)
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(req.body);
    const expectedHmac = hmac.digest('base64');
    
    // Timing-safe comparison
    const isValid = crypto.timingSafeEqual(
      Buffer.from(hmacHeader),
      Buffer.from(expectedHmac)
    );
    
    if (!isValid) {
      return res.status(401).send('Invalid signature');
    }
    
    // Parse and handle the event
    const event = JSON.parse(req.body.toString());
    console.log('Shopify webhook:', event);
    
    res.json({ received: true });
  }
);

app.listen(3000);

Next.js App Router

Route handler
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(req: NextRequest) {
  const body = await req.text();
  const hmacHeader = req.headers.get('x-shopify-hmac-sha256');
  const secret = process.env.SHOPIFY_WEBHOOK_SECRET!;
  
  if (!hmacHeader) {
    return NextResponse.json(
      { error: 'Missing HMAC header' },
      { status: 400 }
    );
  }
  
  // Compute expected HMAC (Base64-encoded)
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(body);
  const expectedHmac = hmac.digest('base64');
  
  // Timing-safe comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(hmacHeader),
    Buffer.from(expectedHmac)
  );
  
  if (!isValid) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 401 }
    );
  }
  
  // Parse and handle the event
  const event = JSON.parse(body);
  console.log('Shopify webhook:', event);
  
  return NextResponse.json({ received: true });
}

Python / Flask

Using hmac module
import hmac
import hashlib
import base64
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
    secret = os.getenv('SHOPIFY_WEBHOOK_SECRET').encode()
    
    if not hmac_header:
        return jsonify({'error': 'Missing HMAC header'}), 400
    
    # Compute expected HMAC (Base64-encoded)
    payload = request.get_data()
    expected_hmac = base64.b64encode(
        hmac.new(secret, payload, hashlib.sha256).digest()
    ).decode()
    
    # Timing-safe comparison
    if not hmac.compare_digest(hmac_header, expected_hmac):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Parse and handle the event
    event = request.get_json()
    print(f"Shopify webhook: {event}")
    
    return jsonify({'success': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Ruby / Rails

Using OpenSSL
# config/routes.rb
post '/webhook', to: 'webhooks#shopify'

# app/controllers/webhooks_controller.rb
require 'openssl'
require 'base64'

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def shopify
    hmac_header = request.headers['X-Shopify-Hmac-SHA256']
    
    unless hmac_header
      render json: { error: 'Missing HMAC header' }, status: 400
      return
    end
    
    payload = request.body.read
    secret = ENV['SHOPIFY_WEBHOOK_SECRET']
    
    # Compute expected HMAC (Base64-encoded)
    expected_hmac = Base64.strict_encode64(
      OpenSSL::HMAC.digest('sha256', secret, payload)
    )
    
    # Timing-safe comparison
    unless Rack::Utils.secure_compare(hmac_header, expected_hmac)
      render json: { error: 'Invalid signature' }, status: 401
      return
    end
    
    # Parse and handle the event
    event = JSON.parse(payload)
    puts "Shopify webhook: #{event}"
    
    render json: { success: true }, status: 200
  end
end

PHP

Using hash_hmac
<?php
// webhook.php

$hmacHeader = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] ?? '';

if (empty($hmacHeader)) {
    http_response_code(400);
    die('Missing HMAC header');
}

$payload = file_get_contents('php://input');
$secret = getenv('SHOPIFY_WEBHOOK_SECRET');

// Compute expected HMAC (Base64-encoded)
$expectedHmac = base64_encode(
    hash_hmac('sha256', $payload, $secret, true)
);

// Timing-safe comparison
if (!hash_equals($hmacHeader, $expectedHmac)) {
    http_response_code(401);
    die('Invalid signature');
}

// Parse and handle the event
$event = json_decode($payload, true);
error_log('Shopify webhook: ' . print_r($event, true));

http_response_code(200);
echo json_encode(['success' => true]);
?>

🔒 Security reminders:

  • Shopify uses Base64 encoding (not hex like GitHub/Stripe)
  • Always use timing-safe comparison functions
  • Verify signatures BEFORE parsing the JSON payload
  • Use the webhook secret, not your API secret key

Frequently Asked Questions

Where do I find my Shopify webhook secret?

To find your webhook secret:

  1. Log in to your Shopify Admin
  2. Go to SettingsNotifications
  3. Scroll down to the Webhooks section
  4. Click on the webhook you want to verify
  5. The secret is shown in the webhook details

Note: This is different from your API secret key. Don't confuse them!

What is the X-Shopify-Hmac-SHA256 header?

The X-Shopify-Hmac-SHA256 header is a security header that Shopify includes in every webhook request. It contains a Base64-encoded HMAC-SHA256 hash of the raw request body, generated using your Shopify webhook secret.

Shopify computes this HMAC-SHA256 signature before sending the webhook. To verify the request, your server must compute the same HMAC-SHA256 hash using the exact raw request body bytes and your webhook secret, then compare the result to the value in the X-Shopify-Hmac-SHA256 header.

If the computed hash matches the header value, the webhook is authentic and was sent by Shopify. If it does not match, the request should be rejected because it may have been modified or forged.

How does Shopify webhook verification work?

Shopify uses HMAC-SHA256 to sign webhook payloads:

  1. Shopify computes an HMAC of the raw request body using your secret
  2. The HMAC is Base64-encoded and sent in the X-Shopify-Hmac-SHA256 header
  3. Your server computes the same HMAC and compares it
  4. If they match, the webhook is authentic

Why does verification fail in my code but work here?

Common causes:

  • Body parsing: Your framework parsed the JSON before verification
  • Encoding mismatch: Shopify uses Base64, not hex like GitHub
  • Wrong secret: Using API key instead of webhook secret
  • Character encoding: Body was converted to/from string incorrectly

Fix: Verify against the raw request body bytes before parsing JSON.

What's the difference between Shopify, Stripe, and GitHub signatures?

FeatureShopifyStripeGitHub
HeaderX-Shopify-Hmac-SHA256Stripe-SignatureX-Hub-Signature-256
EncodingBase64HexHex
AlgorithmHMAC-SHA256HMAC-SHA256HMAC-SHA256
TimestampNoYes (5 min tolerance)No

How do I test Shopify webhooks locally?

Options for local testing:

1. Use ngrok or Cloudflare Tunnel:

ngrok http 3000 # Copy HTTPS URL to Shopify webhook settings

2. Use Shopify CLI:

shopify app dev # Shopify CLI creates a tunnel automatically

3. Manually craft test webhooks:

Use this tool to verify your manually created HMAC signatures match expectations.

What webhook topics does Shopify support?

Shopify supports webhooks for many events:

  • orders/create, orders/updated, orders/paid
  • products/create, products/update, products/delete
  • customers/create, customers/update
  • fulfillments/create, fulfillments/update
  • And many more...

See the Shopify webhook documentation for a complete list.