Webhooks power most real-time integrations in the Shopify ecosystem. When an order is placed, a product is updated, or a customer checks out, Shopify fires an HTTP POST to your endpoint so your systems can react instantly. That convenience comes with a hard security requirement. Any public endpoint can receive a POST, and attackers know it.

Without verification, your webhook consumer trusts every payload that arrives. A spoofed order creation event could trigger fake fulfillment, inflate analytics, or poison downstream queues. This is why Shopify webhook verification sits at the foundation of any serious integration. It confirms that the request genuinely came from Shopify and that nobody tampered with the body in transit.

In this guide, we break down how secure Shopify webhooks work, how to implement webhook HMAC verification correctly, and how to avoid the mistakes that quietly undermine Shopify webhook security in production.

Why Webhook Verification Matters

A Shopify webhook endpoint is, by design, exposed to the open internet. Shopify does not authenticate with a username and password. Instead, it signs each request with a shared secret so you can prove authenticity on your side.

Skipping verification creates real risks:

  • Attackers can inject fake events to manipulate your business logic.
  • Malicious payloads can flood your queues and cause backpressure.
  • Tampered data can corrupt inventory, orders, or financial records.
  • Replayed requests can duplicate side effects such as refunds or emails.

If your architecture relies on event-driven patterns, a single unverified endpoint can compromise the trust model of the entire pipeline. Verification is the gate that everything else depends on.

How Shopify Signs Webhooks

Shopify uses an HMAC SHA-256 signature. HMAC stands for hash-based message authentication code. It combines the raw request body with a shared secret to produce a hash. Shopify sends that hash in the X-Shopify-Hmac-Sha256 header.

Your server computes the same HMAC using the same secret and the exact raw body it received. If the two hashes match, the request is authentic and untampered. If they differ, you reject it.

The shared secret comes from one of two sources depending on how the webhook was created.

Webhook source Secret used for HMAC
Webhook created in Shopify admin or via Admin API The app’s API secret key
Webhook from a Shopify app using app proxy or app config The app client secret
Webhook created via GraphQL webhookSubscriptionCreate The app’s client secret

The critical detail is that both sides must hash the identical byte sequence. Any change to the body, including re-serialization, breaks the match.

The Golden Rule: Use the Raw Body

This is where most webhook verification bugs live. HMAC is computed over the raw request body bytes, not over a parsed and re-serialized version.

Many web frameworks parse JSON automatically before your handler runs. If you then re-stringify that parsed object, the result rarely matches the original bytes. Key order, whitespace, and number formatting can all differ. The HMAC comparison then fails for legitimate requests.

The fix is simple. Capture the raw body before any JSON middleware touches it, and compute HMAC on those exact bytes.

Here is a correct Express implementation using the raw body:

const express = require("express");
const crypto = require("crypto");

const app = express();

// Capture the raw body only for the webhook route
app.use(
  "/webhooks",
  express.raw({ type: "application/json" })
);

function verifyShopifyWebhook(req) {
  const hmacHeader = req.get("X-Shopify-Hmac-Sha256");
  const secret = process.env.SHOPIFY_API_SECRET;

  const generatedHash = crypto
    .createHmac("sha256", secret)
    .update(req.body) // req.body is a Buffer here
    .digest("base64");

  return crypto.timingSafeEqual(
    Buffer.from(generatedHash),
    Buffer.from(hmacHeader)
  );
}

app.post("/webhooks", (req, res) => {
  if (!verifyShopifyWebhook(req)) {
    return res.status(401).send("Unauthorized");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  // Process the verified payload
  res.status(200).send("OK");
});

Notice two things. First, express.raw gives us a Buffer instead of a parsed object. Second, we only parse JSON after verification succeeds.

Constant-Time Comparison Prevents Timing Attacks

A naive comparison with === or == returns as soon as it finds a mismatched character. That tiny timing difference can leak information about how many leading characters matched. Over many requests, an attacker can exploit this to forge a valid signature.

Always use a constant-time comparison function. In Node.js that is crypto.timingSafeEqual. In PHP it is hash_equals. In Python it is hmac.compare_digest.

Here is a PHP example with proper constant-time comparison:

<?php

function verifyShopifyWebhook(string $rawBody, string $hmacHeader): bool
{
    $secret = getenv('SHOPIFY_API_SECRET');

    $calculatedHmac = base64_encode(
        hash_hmac('sha256', $rawBody, $secret, true)
    );

    return hash_equals($calculatedHmac, $hmacHeader);
}

$rawBody = file_get_contents('php://input');
$hmacHeader = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] ?? '';

if (!verifyShopifyWebhook($rawBody, $hmacHeader)) {
    http_response_code(401);
    exit('Unauthorized');
}

http_response_code(200);
echo 'OK';

The true flag in hash_hmac returns raw binary output, which you then base64 encode to match Shopify’s format. Getting this parameter wrong is a common source of mismatches.

A Python Example for Completeness

Teams working in Django, Flask, or FastAPI follow the same pattern. Read the raw body, compute HMAC, compare safely.

import hmac
import hashlib
import base64
import os

def verify_shopify_webhook(raw_body: bytes, hmac_header: str) -> bool:
    secret = os.environ["SHOPIFY_API_SECRET"].encode("utf-8")

    digest = hmac.new(secret, raw_body, hashlib.sha256).digest()
    calculated_hmac = base64.b64encode(digest).decode("utf-8")

    return hmac.compare_digest(calculated_hmac, hmac_header)

Pass the raw request body bytes directly. In FastAPI, use await request.body(). In Flask, use request.get_data() before any JSON parsing.

Verification Is Only the First Layer

Authenticating the sender proves the request is genuine. It does not guarantee your processing is safe, correct, or reliable. A complete security posture layers several controls on top of HMAC.

Layer Purpose
HMAC verification Confirms the request came from Shopify
HTTPS only Prevents interception and tampering in transit
Idempotency keys Stops duplicate side effects from retries
Replay protection Rejects old or repeated requests
Rate and size limits Protects against abuse and payload floods
Logging and monitoring Detects anomalies and failed verifications

Each layer closes a gap the previous one leaves open. HMAC alone, for example, does nothing to stop a valid request from being delivered twice.

Handling Duplicate and Replayed Events

Shopify guarantees at-least-once delivery, not exactly-once. That means a verified, authentic webhook can arrive more than once. If your handler charges a card or sends an email on every delivery, duplicates cause real damage.

The solution is idempotency. Every webhook includes an X-Shopify-Webhook-Id header. Store that ID the first time you process the event, and skip any event whose ID you have already seen.

async function processWebhook(req) {
  const webhookId = req.get("X-Shopify-Webhook-Id");

  const alreadyProcessed = await cache.exists(`wh:${webhookId}`);
  if (alreadyProcessed) {
    return; // Safe no-op for duplicates
  }

  await cache.set(`wh:${webhookId}`, "1", "EX", 86400);
  // Continue with business logic
}

This simple check pairs well with deeper idempotency strategies and dedicated techniques for handling duplicate Shopify events. A fast lookup store, often backed by your caching layers, keeps this check cheap even under heavy load.

Respond Fast, Process Later

Shopify expects a 200 response within a few seconds. If your endpoint is slow, Shopify marks the delivery as failed and retries it. Repeated failures can get your webhook subscription removed entirely.

The safest pattern is to verify, acknowledge, then process asynchronously. Verify the HMAC, push the payload onto a queue, and return 200 immediately. A separate worker then handles the real work.

This decoupling is the heart of queue-based webhook processing and broader async event processing architectures. It keeps your response times predictable and your business logic isolated from delivery timing.

When processing fails, a dead letter queue captures the failed job for later inspection instead of losing it. Combined with sensible retry strategies, this gives you durable, recoverable webhook handling.

Verification at Scale

A single verification call is cheap. Millions of them during a flash sale are not trivial. HMAC computation is CPU bound, and raw body buffering uses memory.

A few practices keep verification efficient as volume grows:

  • Keep the secret in memory, not fetched per request.
  • Verify before any expensive parsing or database work.
  • Reject invalid requests early to free resources fast.
  • Spread load across instances with load balancing.

If you are scaling webhook infrastructure, verification should sit at the very edge of your pipeline. Rejecting bad traffic early protects your queue infrastructure and downstream workers from wasted cycles. These concerns are central to any high-traffic Shopify architecture.

Common Verification Mistakes

Even experienced teams trip over the same issues. Here are the ones that cause the most support tickets.

Mistake Consequence Fix
Using parsed JSON for HMAC All valid requests fail Use the raw body bytes
Wrong secret key Signature never matches Use the app’s correct secret
Non constant-time compare Timing attack exposure Use a safe compare function
Slow synchronous processing Shopify retries and removes webhook Queue and respond fast
Ignoring duplicate deliveries Double charges, double emails Deduplicate by webhook ID
No logging on failures Silent breakage, hard to debug Log and alert on rejects

The raw body mistake deserves special emphasis. It is the single most common reason verification fails in a fresh integration, and it is invisible until you test with a real Shopify payload.

Monitoring and Observability

Verification failures are signals, not just errors. A sudden spike in rejected requests might mean a rotated secret, a misconfigured proxy, or an active attack. You cannot react to what you cannot see.

Instrument your endpoint to track verification outcomes, processing latency, and queue depth. This feeds directly into webhook monitoring and observability and broader observability across your Shopify ecosystem. When something breaks at 2 a.m., good telemetry turns a vague outage into a clear incident response path.

Track at minimum:

  • Count of valid versus invalid signatures.
  • Time to respond with 200.
  • Queue depth and worker throughput.
  • Duplicate detection rate.

Rising invalid signatures right after a deploy usually means the raw body handling broke. Rising duplicates might point to slow responses triggering Shopify retries.

Building for Reliability

Secure verification is one pillar of a resilient webhook system. The others are reliability and fault tolerance. A verified event is useless if your consumer crashes before processing it.

Design your consumers to be restartable and idempotent, so a crash mid-process does not corrupt state. These principles underpin reliable Shopify webhook consumers and fault-tolerant Shopify integration systems. For systems that sync critical data such as stock levels, the same discipline applies to real-time inventory synchronization and distributed inventory sync.

Order also matters. Shopify does not guarantee events arrive in the order they occurred. If you process an order update before the order creation, your state goes wrong. Review webhook ordering problems to design handlers that tolerate out-of-order delivery, and consider a replay system for recovering missed events.

Rotating Secrets Safely

Secrets should rotate periodically, and sometimes urgently after a suspected leak. The challenge is that rotation can break verification if you cut over instantly.

Support a short grace period where your endpoint accepts either the old or the new secret. Compute HMAC against both and accept if either matches. Once all webhooks use the new secret, remove the old one.

function verifyWithRotation(rawBody, hmacHeader, secrets) {
  return secrets.some((secret) => {
    const hash = crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("base64");
    try {
      return crypto.timingSafeEqual(
        Buffer.from(hash),
        Buffer.from(hmacHeader)
      );
    } catch {
      return false;
    }
  });
}

Keep the grace window short. Every extra accepted secret widens your attack surface.

Putting It All Together

A production-grade webhook endpoint follows a clear, ordered flow. Each step gates the next, and the fast path stays lean.

  1. Read the raw request body as bytes.
  2. Verify the HMAC using a constant-time compare.
  3. Reject with 401 if verification fails.
  4. Check the webhook ID for duplicates.
  5. Push the payload onto a queue.
  6. Return 200 immediately.
  7. Process the event in a separate worker.
  8. Log outcomes and emit metrics throughout.

This sequence satisfies both Shopify webhook security and operational reliability. It verifies authenticity, protects against replays, keeps response times low, and isolates failure. As you grow, layer in backpressure handling so a traffic surge degrades gracefully instead of collapsing.

Verification is not a one-time task you check off. It is a living part of your architecture that deserves monitoring, testing, and periodic review. Treat it with the same care you give to payment processing or data integrity, because a compromised webhook pipeline can undermine all three.

If your team is building or hardening these systems, strong verification is the right place to start. From there, the patterns above help you scale confidently without trading away security.

Frequently Asked Questions

What is Shopify webhook verification?

It is the process of confirming that an incoming webhook genuinely came from Shopify and was not tampered with. Shopify signs each request with an HMAC SHA-256 hash that your server recomputes and compares.

Why does my HMAC comparison keep failing?

The most common cause is using parsed and re-serialized JSON instead of the raw request body. HMAC must be computed over the exact raw bytes Shopify sent, before any JSON middleware runs.

Which secret should I use for webhook HMAC verification?

Use your app’s API secret key or client secret, depending on how the webhook was created. Webhooks from the admin or Admin API use the app secret, and misusing this key is a frequent cause of failed verification.

Can verified webhooks still be duplicates?

Yes. Shopify guarantees at-least-once delivery, so a valid webhook may arrive multiple times. Deduplicate using the X-Shopify-Webhook-Id header and idempotent processing to avoid repeated side effects.

How fast must my webhook endpoint respond?

Respond with a 200 status within a few seconds. The safest approach is to verify the HMAC, queue the payload, return 200 immediately, and process the event asynchronously in a separate worker.

Why use constant-time comparison for HMAC?

A normal comparison exits early on the first mismatched character, which can leak timing information an attacker could exploit. Constant-time functions like timingSafeEqual or hash_equals compare safely regardless of where a mismatch occurs.

Your Trusted Shopify Partner.

Get in touch with our expert Shopify consultants today and let’s discuss your ideas and business requirements.