How to Test Shopify Webhooks

Create a store-level webhook or an app subscription, inspect the X-Shopify headers, verify X-Shopify-Hmac-Sha256 and handle Shopify's timeout and retry rules.

Updated:

Shopify webhooks tell your app when an order is created, a product changes or an app is uninstalled. To build a handler that doesn't miss events, you need to know the payload shape, the headers, how the HMAC is computed and how strict Shopify is about response times. This guide covers each, using a public capture URL to look at real deliveries first.

Two ways to create Shopify webhooks

Store-level webhook App subscription
Where Shopify admin: Settings → Notifications → Webhooks App config (shopify.app.toml) or the Admin API
Who Merchants and developers testing one store Apps installed on stores
Signing key Key shown in the admin's webhooks section Your app's client secret

Both deliver the same kind of request: a POST with a JSON body (store-level webhooks can also use XML) and a set of X-Shopify-* headers.

Step 1: Create a capture URL

Open Webhook Inspector and create a URL, for example:

https://webhook.voronyuk.com/abcd23efgh/shopify/webhooks

The default response is 200 with {"ok":true,"request_id":N}, so Shopify treats deliveries as successful.

Step 2a: Store-level webhook

  1. In the Shopify admin, go to Settings → Notifications and open the Webhooks section.
  2. Click Create webhook.
  3. Choose an event, such as Order creation, the format (JSON) and the API version.
  4. Paste the capture URL and save.

Then place a test order or update a product. The delivery appears in the inspector in real time.

Step 2b: App subscription

For an app built with Shopify CLI, declare subscriptions in shopify.app.toml:

[webhooks]
api_version = "2026-07"

[[webhooks.subscriptions]]
topics = ["orders/create", "products/update"]
uri = "https://webhook.voronyuk.com/abcd23efgh/shopify/webhooks"

Deploy the config, trigger the event in a development store and watch the request arrive. Common topics include orders/create, orders/paid, products/update, customers/create and app/uninstalled.

Step 3: Read the headers

Header Meaning
X-Shopify-Topic The topic, e.g. orders/create
X-Shopify-Hmac-Sha256 Base64 HMAC-SHA256 of the raw body
X-Shopify-Shop-Domain The store's myshopify.com domain
X-Shopify-Webhook-Id Unique per delivery; use it to detect duplicates
X-Shopify-Event-Id Shared by deliveries caused by the same merchant action
X-Shopify-API-Version API version used to serialize the payload
X-Shopify-Triggered-At When Shopify triggered the delivery

Header names are case-insensitive, so don't compare them with exact casing. The inspector lets you search across all headers, which is handy for finding every delivery from one shop domain or topic.

Step 4: Verify X-Shopify-Hmac-Sha256

Shopify computes an HMAC-SHA256 of the raw request body using the secret as the key, then base64-encodes it. Compute the same digest and compare in constant time.

Node.js

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

const SECRET = process.env.SHOPIFY_WEBHOOK_SECRET; // app client secret
const app = express();

function verifyShopify(rawBody, hmacHeader) {
  if (!hmacHeader) return false;
  const digest = crypto.createHmac('sha256', SECRET).update(rawBody).digest();
  const received = Buffer.from(hmacHeader, 'base64');
  return digest.length === received.length && crypto.timingSafeEqual(digest, received);
}

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyShopify(req.body, req.get('X-Shopify-Hmac-Sha256'))) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // acknowledge immediately
  const topic = req.get('X-Shopify-Topic');
  const payload = JSON.parse(req.body.toString('utf8'));
  console.log(topic, req.get('X-Shopify-Webhook-Id'), payload.id);
});

app.listen(3000);

Python

import base64
import hashlib
import hmac
import os

from flask import Flask, abort, request

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


@app.post("/webhooks")
def webhooks():
    digest = hmac.new(SECRET, request.get_data(), hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    if not hmac.compare_digest(expected, request.headers.get("X-Shopify-Hmac-Sha256", "")):
        abort(401)
    print(request.headers.get("X-Shopify-Topic"), request.headers.get("X-Shopify-Webhook-Id"))
    return "", 200


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

If verification fails, check which secret you're using: app subscriptions are signed with the app's client secret, store-level webhooks with the key shown in the admin.

Step 5: Forward to localhost

In the Forwarding panel, set the target to http://localhost:3000. The captured sub-path is appended, so .../abcd23efgh/shopify/webhooks arrives at http://localhost:3000/webhooks. Use the browser relay (your server must allow CORS) or the CLI agent:

WI_TOKEN='<token>' inspector listen https://webhook.voronyuk.com/abcd23efgh/shopify --to localhost:3000

Copy the exact command with your token from the panel. More options in how to test webhooks locally.

Response time and retries

  • 5 seconds. Shopify expects your app to respond within five seconds; otherwise the delivery fails. Verify, respond 200, then process in a background job.
  • Retries. A failed delivery is retried up to eight times over a four-hour period, with growing intervals.
  • Removal. If failures continue, the subscription can be removed and you'll need to recreate it after fixing the endpoint.
  • Duplicates. Retries mean the same delivery can arrive more than once. Deduplicate on X-Shopify-Webhook-Id.

You can safely rehearse failures: set the capture URL's custom response to a 500 or a delay, and see what happens. When your handler breaks, replay the stored delivery from the Shopify webhook tester instead of placing another test order.

Related: Stripe webhooks and Twilio webhooks.

FAQ

How do I test Shopify webhooks without a public server?

Register a capture URL as the webhook address, inspect the deliveries, then forward them to localhost with the CLI agent or browser relay.

Why is my Shopify HMAC verification failing?

Most often the body was parsed before hashing. Use the raw bytes. Also make sure you compare against the base64 digest and use the correct secret for the webhook type.

How long does Shopify wait for a webhook response?

Five seconds. Slower responses count as failed deliveries.

How many times does Shopify retry a webhook?

Up to eight times over four hours. Persistent failures can lead to the subscription being removed.

How do I avoid processing the same Shopify webhook twice?

Store the X-Shopify-Webhook-Id of each processed delivery and skip any ID you've already seen.

Try it now

Get a free webhook URL and watch the requests arrive live — no signup.

Get a webhook URL